diff --git a/.changeset/beige-feet-tap.md b/.changeset/beige-feet-tap.md deleted file mode 100644 index b37d249700..0000000000 --- a/.changeset/beige-feet-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@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 deleted file mode 100644 index 35feed552b..0000000000 --- a/.changeset/blue-jobs-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@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/bright-nails-grin.md b/.changeset/bright-nails-grin.md deleted file mode 100644 index 434a17efbb..0000000000 --- a/.changeset/bright-nails-grin.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -'@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/chilled-dolls-agree.md b/.changeset/chilled-dolls-agree.md new file mode 100644 index 0000000000..750307bf53 --- /dev/null +++ b/.changeset/chilled-dolls-agree.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-backend/alpha` instead of `@backstage/plugin-catalog-backend`. + +- `catalogConditions` +- `createCatalogPolicyDecision` +- `permissionRules` +- `createCatalogPermissionRule` diff --git a/.changeset/chilled-items-trade.md b/.changeset/chilled-items-trade.md new file mode 100644 index 0000000000..b069885498 --- /dev/null +++ b/.changeset/chilled-items-trade.md @@ -0,0 +1,13 @@ +--- +'@backstage/backend-common': minor +--- + +**BREAKING**: The connection string for `redis` cache store now requires a protocol prefix. + +```diff +backend: + cache: + store: redis +- connection: user:pass@cache.example.com:6379 ++ connection: redis://user:pass@cache.example.com:6379 +``` diff --git a/.changeset/chilly-goats-suffer.md b/.changeset/chilly-goats-suffer.md new file mode 100644 index 0000000000..da98b4019b --- /dev/null +++ b/.changeset/chilly-goats-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Applied fix from version 0.17.2 of this package, which is part of the v0.69.2 release of Backstage. diff --git a/.changeset/dependabot-8a13aed.md b/.changeset/dependabot-8a13aed.md new file mode 100644 index 0000000000..13075ccde2 --- /dev/null +++ b/.changeset/dependabot-8a13aed.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-search': patch +'@backstage/plugin-techdocs': patch +--- + +chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 diff --git a/.changeset/dull-months-knock.md b/.changeset/dull-months-knock.md new file mode 100644 index 0000000000..3b957518c2 --- /dev/null +++ b/.changeset/dull-months-knock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Added a new interface that allows for customization of when to build techdocs diff --git a/.changeset/eight-adults-joke.md b/.changeset/eight-adults-joke.md new file mode 100644 index 0000000000..ffda738213 --- /dev/null +++ b/.changeset/eight-adults-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-analytics-module-ga': patch +--- + +Added CSP instructions to README diff --git a/.changeset/fast-ducks-sell.md b/.changeset/fast-ducks-sell.md deleted file mode 100644 index 2ad654124d..0000000000 --- a/.changeset/fast-ducks-sell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Support "dependencyOf" relation in Resource entities diff --git a/.changeset/fast-lizards-fly.md b/.changeset/fast-lizards-fly.md deleted file mode 100644 index e6772b056f..0000000000 --- a/.changeset/fast-lizards-fly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Bump `passport-oauth2` to version 1.6.1 diff --git a/.changeset/fast-paws-arrive.md b/.changeset/fast-paws-arrive.md new file mode 100644 index 0000000000..9c5f4f444f --- /dev/null +++ b/.changeset/fast-paws-arrive.md @@ -0,0 +1,31 @@ +--- +'@backstage/create-app': patch +--- + +Update the template to reflect the renaming of `DocsResultListItem` to `TechDocsSearchResultListItem` from `@backstage/plugin-techdocs`. + +To apply this change to an existing app, make the following change to `packages/app/src/components/search/SearchPage.tsx`: + +```diff +-import { DocsResultListItem } from '@backstage/plugin-techdocs'; ++import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; +``` + +```diff + case 'techdocs': + return ( +- +``` + +The `TechDocsIndexPage` now uses `DefaultTechDocsHome` as fall back if no children is provided as `LegacyTechDocsHome` is marked as deprecated. If you do not use a custom techdocs homepage, you can therefore update your app to the following: + +```diff +- }> +- +- ++ } /> +``` diff --git a/.changeset/flat-crabs-smash.md b/.changeset/flat-crabs-smash.md deleted file mode 100644 index b56b340605..0000000000 --- a/.changeset/flat-crabs-smash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@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/giant-taxis-drop.md b/.changeset/giant-taxis-drop.md new file mode 100644 index 0000000000..41f8a91d40 --- /dev/null +++ b/.changeset/giant-taxis-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-common': minor +--- + +Remove resourceType property from catalogEntityCreatePermission. Resource type refers to the type of resources whose resourceRefs should be passed along with authorize requests, to allow conditional responses for that resource type. Since creation does not correspond to an entity (as the entity does not exist at the time of authorization), the resourceRef should not be included on the permission. diff --git a/.changeset/gorgeous-actors-shave.md b/.changeset/gorgeous-actors-shave.md new file mode 100644 index 0000000000..bcf5f43018 --- /dev/null +++ b/.changeset/gorgeous-actors-shave.md @@ -0,0 +1,12 @@ +--- +'@backstage/create-app': patch +--- + +Update import location of catalogEntityCreatePermission. + +To apply this change to an existing app, make the following change to `packages/app/src/App.tsx`: + +```diff +-import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; ++import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; +``` diff --git a/.changeset/gorgeous-boats-hide.md b/.changeset/gorgeous-boats-hide.md new file mode 100644 index 0000000000..5c60240433 --- /dev/null +++ b/.changeset/gorgeous-boats-hide.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-org': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-techdocs': patch +--- + +- **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. diff --git a/.changeset/honest-shrimps-clap.md b/.changeset/honest-shrimps-clap.md deleted file mode 100644 index 12ae623704..0000000000 --- a/.changeset/honest-shrimps-clap.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@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/honest-students-clean.md b/.changeset/honest-students-clean.md new file mode 100644 index 0000000000..ca8db28343 --- /dev/null +++ b/.changeset/honest-students-clean.md @@ -0,0 +1,7 @@ +--- +'@backstage/integration': minor +'@backstage/backend-common': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults diff --git a/.changeset/khaki-socks-wash.md b/.changeset/khaki-socks-wash.md new file mode 100644 index 0000000000..241ac75be3 --- /dev/null +++ b/.changeset/khaki-socks-wash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Set timeout for scaffolder octokit client diff --git a/.changeset/large-dancers-learn.md b/.changeset/large-dancers-learn.md new file mode 100644 index 0000000000..9f25990924 --- /dev/null +++ b/.changeset/large-dancers-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +- **BREAKING**: Removed the `FavouriteTemplate` export in favor of the `FavoriteEntity` from `@backstage/plugin-catalog-react`. Please migrate any usages to that component instead if you are creating your own `TemplateCard` page. diff --git a/.changeset/lemon-needles-applaud.md b/.changeset/lemon-needles-applaud.md new file mode 100644 index 0000000000..a77d3fd33c --- /dev/null +++ b/.changeset/lemon-needles-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +**DEPRECATED**: The `results` export, and instead adding `processingResult` with the same shape and purpose. diff --git a/.changeset/little-carpets-itch.md b/.changeset/little-carpets-itch.md new file mode 100644 index 0000000000..6a4d95c140 --- /dev/null +++ b/.changeset/little-carpets-itch.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Added some deprecations as follows: + +- **DEPRECATED**: `TemplateCardComponent` and `TaskPageComponent` props have been deprecated, and moved to a `components` prop instead. You can pass them in through there instead. +- **DEPRECATED**: `TemplateList` and `TemplateListProps` has been deprecated. Please use the `TemplateCard` to create your own list component instead. +- **DEPRECATED**: `setSecret` has been deprecated in favour of `setSecrets` when calling `useTemplateSecrets` + +Other notable changes: + +- `scaffolderApi.scaffold()` `values` type has been narrowed from `Record` to `Record` instead. +- Moved all navigation internally over to using `routeRefs` and `subRouteRefs` diff --git a/.changeset/little-ravens-kick.md b/.changeset/little-ravens-kick.md deleted file mode 100644 index 1e14f2f37e..0000000000 --- a/.changeset/little-ravens-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@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/long-weeks-thank.md b/.changeset/long-weeks-thank.md new file mode 100644 index 0000000000..a6c3a0719f --- /dev/null +++ b/.changeset/long-weeks-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Internal restructuring to collect the various provider files in a `modules` folder while waiting to be externalized diff --git a/.changeset/lovely-goats-press.md b/.changeset/lovely-goats-press.md new file mode 100644 index 0000000000..fc3c0b4763 --- /dev/null +++ b/.changeset/lovely-goats-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Collapse techdocs sidebar on small devices diff --git a/.changeset/new-foxes-matter.md b/.changeset/new-foxes-matter.md new file mode 100644 index 0000000000..234dd78451 --- /dev/null +++ b/.changeset/new-foxes-matter.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-app-api': minor +'@backstage/core-plugin-api': minor +'@backstage/test-utils': minor +--- + +**BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use `observe$`. diff --git a/.changeset/nice-dragons-collect.md b/.changeset/nice-dragons-collect.md new file mode 100644 index 0000000000..b3d9efefd9 --- /dev/null +++ b/.changeset/nice-dragons-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-common': patch +--- + +Add api doc comments to `Permission` type properties. diff --git a/.changeset/nice-walls-reply.md b/.changeset/nice-walls-reply.md new file mode 100644 index 0000000000..f6d1fb7562 --- /dev/null +++ b/.changeset/nice-walls-reply.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-catalog-common': minor +--- + +**Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-common/alpha` instead of `@backstage/plugin-catalog-common`. + +- `RESOURCE_TYPE_CATALOG_ENTITY` +- `catalogEntityReadPermission` +- `catalogEntityCreatePermission` +- `catalogEntityDeletePermission` +- `catalogEntityRefreshPermission` +- `catalogLocationReadPermission` +- `catalogLocationCreatePermission` +- `catalogLocationDeletePermission` diff --git a/.changeset/ninety-kids-drop.md b/.changeset/ninety-kids-drop.md new file mode 100644 index 0000000000..ed5cf6330b --- /dev/null +++ b/.changeset/ninety-kids-drop.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Removed some previously deprecated `routeRefs` as follows: + +- **BREAKING**: Removed `entityRoute` in favor of `entityRouteRef`. +- **BREAKING**: Removed the previously deprecated `rootRoute` and `catalogRouteRef`. If you want to refer to the catalog index page from a public plugin you now need to use an `ExternalRouteRef` instead. For private plugins it is possible to take the shortcut of referring directly to `catalogPlugin.routes.indexPage` instead. diff --git a/.changeset/old-waves-wash.md b/.changeset/old-waves-wash.md new file mode 100644 index 0000000000..34ba0760b5 --- /dev/null +++ b/.changeset/old-waves-wash.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/plugin-catalog-backend-module-ldap': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Use the new `processingResult` export from the catalog backend diff --git a/.changeset/olive-flies-reflect.md b/.changeset/olive-flies-reflect.md deleted file mode 100644 index 80075d8fb0..0000000000 --- a/.changeset/olive-flies-reflect.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@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/olive-glasses-approve.md b/.changeset/olive-glasses-approve.md new file mode 100644 index 0000000000..e74b7ef485 --- /dev/null +++ b/.changeset/olive-glasses-approve.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Export FetchUrlReader to facilitate more flexible configuration of the backend. diff --git a/.changeset/olive-lobsters-rescue.md b/.changeset/olive-lobsters-rescue.md new file mode 100644 index 0000000000..37a7712782 --- /dev/null +++ b/.changeset/olive-lobsters-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +**BREAKING**: Removed export of `GithubSession` and `SamlSession` which are only used internally. diff --git a/.changeset/orange-crews-explain.md b/.changeset/orange-crews-explain.md new file mode 100644 index 0000000000..4b831392eb --- /dev/null +++ b/.changeset/orange-crews-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': minor +--- + +**BREAKING**: Removed the deprecated `app.` template variables from the `index.html` templating. These should be replaced by using `config.getString("app.")` instead. diff --git a/.changeset/patched.json b/.changeset/patched.json index 1029c8f106..486cfe13a5 100644 --- a/.changeset/patched.json +++ b/.changeset/patched.json @@ -1,3 +1,5 @@ { - "currentReleaseVersion": {} + "currentReleaseVersion": { + "@backstage/plugin-scaffolder-backend": "0.17.2" + } } diff --git a/.changeset/pink-onions-ring.md b/.changeset/pink-onions-ring.md deleted file mode 100644 index 2b81ad4b43..0000000000 --- a/.changeset/pink-onions-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@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/lazy-jeans-obey.md b/.changeset/plenty-tables-mix.md similarity index 51% rename from .changeset/lazy-jeans-obey.md rename to .changeset/plenty-tables-mix.md index 9124b983dc..5dffdf6946 100644 --- a/.changeset/lazy-jeans-obey.md +++ b/.changeset/plenty-tables-mix.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend': patch --- -Update internal `Location` validation. +Deprecated the `BitbucketRepositoryParser` type. diff --git a/.changeset/polite-dots-train.md b/.changeset/polite-dots-train.md deleted file mode 100644 index dbd11d3e60..0000000000 --- a/.changeset/polite-dots-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-yeoman': patch ---- - -Bump `yeoman-environment` dependency from `^3.6.0` to `^3.9.1`. diff --git a/.changeset/polite-poems-nail.md b/.changeset/polite-poems-nail.md new file mode 100644 index 0000000000..c3eb97fce0 --- /dev/null +++ b/.changeset/polite-poems-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Marked `useEntityPermission` as alpha since the underlying permission framework is under active development. diff --git a/.changeset/poor-hounds-beam.md b/.changeset/poor-hounds-beam.md new file mode 100644 index 0000000000..925370c1b0 --- /dev/null +++ b/.changeset/poor-hounds-beam.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The CLI now bundles both version 16 and 17 of the patched `@hot-loader/react-dom` dependency, and selects the appropriate one based on what version of `react-dom` is installed within the app. diff --git a/.changeset/quick-mugs-pay.md b/.changeset/quick-mugs-pay.md new file mode 100644 index 0000000000..f2d3a00eb8 --- /dev/null +++ b/.changeset/quick-mugs-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +add documentation for config options `userGroupMemberSearch` and `groupSearch` diff --git a/.changeset/rare-insects-punch.md b/.changeset/rare-insects-punch.md new file mode 100644 index 0000000000..d22b851abd --- /dev/null +++ b/.changeset/rare-insects-punch.md @@ -0,0 +1,10 @@ +--- +'@backstage/app-defaults': minor +'@backstage/core-app-api': minor +'@backstage/core-components': minor +'@backstage/core-plugin-api': minor +'@backstage/test-utils': minor +'@backstage/plugin-user-settings': minor +--- + +**BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). diff --git a/.changeset/real-kids-hide.md b/.changeset/real-kids-hide.md new file mode 100644 index 0000000000..188a4061c9 --- /dev/null +++ b/.changeset/real-kids-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**Breaking**: Removed `entityRef` from `CatalogProcessorRelationResult`. The field is not used by the catalog and relation information is already available inside the `reation` property. diff --git a/.changeset/red-chefs-beam.md b/.changeset/red-chefs-beam.md new file mode 100644 index 0000000000..5cadccb0aa --- /dev/null +++ b/.changeset/red-chefs-beam.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-airbrake': patch +'@backstage/plugin-airbrake-backend': patch +--- + +The Airbrake plugin installation instructions have been updated to work better and conform to how the frontend and backend plugins are supposed to be integrated into a Backstage instance. diff --git a/.changeset/search-blankly-have-a-nice-life.md b/.changeset/search-blankly-have-a-nice-life.md new file mode 100644 index 0000000000..63dc6051bd --- /dev/null +++ b/.changeset/search-blankly-have-a-nice-life.md @@ -0,0 +1,18 @@ +--- +'@backstage/plugin-search-backend-node': minor +'@backstage/search-common': minor +--- + +**BREAKING** + +The Backstage Search Platform's indexing process has been rewritten as a stream +pipeline in order to improve efficiency and performance on large document sets. + +The concepts of `Collator` and `Decorator` have been replaced with readable and +transform object streams (respectively), as well as factory classes to +instantiate them. Accordingly, the `SearchEngine.index()` method has also been +replaced with a `getIndexer()` factory method that resolves to a writable +object stream. + +Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) +for further details. diff --git a/.changeset/search-done-me-no-favor.md b/.changeset/search-done-me-no-favor.md new file mode 100644 index 0000000000..0b4e24c4a8 --- /dev/null +++ b/.changeset/search-done-me-no-favor.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-search-backend-module-pg': minor +--- + +**BREAKING** + +The `PgSearchEngine` implements the new stream-based indexing process expected +by the latest `@backstage/search-backend-node`. + +When updating to this version, you must also update to the latest version of +`@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) +for further details. diff --git a/.changeset/search-just-smile-politely.md b/.changeset/search-just-smile-politely.md new file mode 100644 index 0000000000..a7831155da --- /dev/null +++ b/.changeset/search-just-smile-politely.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +A `DefaultTechDocsCollatorFactory`, which works with the new stream-based +search indexing subsystem, is now available. The `DefaultTechDocsCollator` will +continue to be available for those unable to upgrade to the stream-based +`@backstage/search-backend-node` (and related packages), however it is now +marked as deprecated and will be removed in a future version. + +To upgrade this plugin and the search indexing subsystem in one go, check +[this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) +for necessary changes to your search backend plugin configuration. diff --git a/.changeset/search-like-a-bank-teller.md b/.changeset/search-like-a-bank-teller.md new file mode 100644 index 0000000000..a9b0330eb2 --- /dev/null +++ b/.changeset/search-like-a-bank-teller.md @@ -0,0 +1,43 @@ +--- +'@backstage/create-app': patch +--- + +The Backstage Search Platform's indexing process has been rewritten as a stream +pipeline in order to improve efficiency and performance on large document sets. + +To take advantage of this, upgrade to the latest version of +`@backstage/plugin-search-backend-node`, as well as any backend plugins whose +collators you are using. Then, make the following changes to your +`/packages/backend/src/plugins/search.ts` file: + +```diff +-import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; +-import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; ++import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; ++import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; + +// ... + + const indexBuilder = new IndexBuilder({ logger, searchEngine }); + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, +- collator: DefaultCatalogCollator.fromConfig(config, { discovery }), ++ factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }), + }); + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, +- collator: DefaultTechDocsCollator.fromConfig(config, { ++ factory: DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery, + logger, + }), + }); +``` + +If you've written custom collators, decorators, or search engines in your +Backstage backend instance, you will need to re-implement them as readable, +transform, and writable streams respectively (including factory classes for +instantiating them). [A how-to guide for refactoring](https://backstage.io/docs/features/search/how-to-guides#rewriting-alpha-style-collators-for-beta) +existing implementations is available. diff --git a/.changeset/search-selfless-cold-composed.md b/.changeset/search-selfless-cold-composed.md new file mode 100644 index 0000000000..7e7dafd421 --- /dev/null +++ b/.changeset/search-selfless-cold-composed.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': minor +--- + +**BREAKING** + +The `ElasticSearchSearchEngine` implements the new stream-based indexing +process expected by the latest `@backstage/search-backend-node`. + +When updating to this version, you must also update to the latest version of +`@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) +for further details. diff --git a/.changeset/search-throw-me-a-right.md b/.changeset/search-throw-me-a-right.md new file mode 100644 index 0000000000..32f4e82fe4 --- /dev/null +++ b/.changeset/search-throw-me-a-right.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +A `DefaultCatalogCollatorFactory`, which works with the new stream-based +search indexing subsystem, is now available. The `DefaultCatalogCollator` will +continue to be available for those unable to upgrade to the stream-based +`@backstage/search-backend-node` (and related packages), however it is now +marked as deprecated and will be removed in a future version. + +To upgrade this plugin and the search indexing subsystem in one go, check +[this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) +for necessary changes to your search backend plugin configuration. diff --git a/.changeset/serious-mayflies-thank.md b/.changeset/serious-mayflies-thank.md new file mode 100644 index 0000000000..b3c5e31527 --- /dev/null +++ b/.changeset/serious-mayflies-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**Breaking**: Removed optional `handleError()` from `CatalogProcessor`. This optional method is never called by the catalog processing engine and can therefore be removed. diff --git a/.changeset/shaggy-readers-explain.md b/.changeset/shaggy-readers-explain.md new file mode 100644 index 0000000000..29266e5a32 --- /dev/null +++ b/.changeset/shaggy-readers-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Cleanup API report diff --git a/.changeset/silly-ghosts-dance.md b/.changeset/silly-ghosts-dance.md deleted file mode 100644 index e83fc87521..0000000000 --- a/.changeset/silly-ghosts-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Improve overall appearance of highlighted code in docs. diff --git a/.changeset/silver-boxes-flash.md b/.changeset/silver-boxes-flash.md new file mode 100644 index 0000000000..9f30c1a34b --- /dev/null +++ b/.changeset/silver-boxes-flash.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +**BREAKING**: Moved **DefaultStarredEntitiesApi** to `@backstage/plugin-catalog`. If you were using this in tests, you can use the new `MockStarredEntitiesApi` from `@backstage/plugin-catalog-react` instead. + +Fixed a risky behavior where `DefaultStarredEntitiesApi` forwarded values to observers that were later mutated. + +Removed the `isStarred` method from `DefaultStarredEntitiesApi`, as it is not part of the `StarredEntitiesApi`. diff --git a/.changeset/silver-chairs-compete.md b/.changeset/silver-chairs-compete.md new file mode 100644 index 0000000000..356cd2043b --- /dev/null +++ b/.changeset/silver-chairs-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +**BREAKING**: OAuth provider id is now required when passing a provider to `createAuthRequester`. diff --git a/.changeset/smart-items-fry.md b/.changeset/smart-items-fry.md new file mode 100644 index 0000000000..625db82ce7 --- /dev/null +++ b/.changeset/smart-items-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Applied fix from `v0.17.1` of this package which is part of the `v0.69.1` release of Backstage. diff --git a/.changeset/sour-eggs-kick.md b/.changeset/sour-eggs-kick.md new file mode 100644 index 0000000000..f796b173fb --- /dev/null +++ b/.changeset/sour-eggs-kick.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-react': patch +--- + +Added the following deprecations to the `catalog-react` package: + +- **DEPRECATION**: `useEntity` will now warn if the entity has not yet been loaded, and will soon throw errors instead. If you're using the default implementation of `EntityLayout` and `EntitySwitch` then these components will ensure that there is an entity loaded before rendering children. If you're implementing your own `EntityLayout` or `EntitySwitch` or something that operates outside or adjacent to them, then use `useAsyncEntity`. + +- **DEPRECATION**: the `loading`, `error` and `refresh` properties that are returned from `useEntity` have been deprecated, and are available on `useAsyncEntity` instead. diff --git a/.changeset/sour-hats-love.md b/.changeset/sour-hats-love.md deleted file mode 100644 index f2cc88e66a..0000000000 --- a/.changeset/sour-hats-love.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Updated `isDatabaseConflictError` to handle modern sqlite conflict errors diff --git a/.changeset/spicy-doors-relax.md b/.changeset/spicy-doors-relax.md new file mode 100644 index 0000000000..3007f50d0e --- /dev/null +++ b/.changeset/spicy-doors-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Deprecated `getEntityMetadataEditUrl` and `getEntityMetadataViewUrl` as these just return one annotation from the entity passed in. diff --git a/.changeset/spicy-lies-grin.md b/.changeset/spicy-lies-grin.md new file mode 100644 index 0000000000..b7e020f1db --- /dev/null +++ b/.changeset/spicy-lies-grin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': patch +--- + +Fixed bug in Catalog LDAP module to acknowledge page events to continue receiving entries if pagePause=true diff --git a/.changeset/spicy-onions-kiss.md b/.changeset/spicy-onions-kiss.md new file mode 100644 index 0000000000..777e233b88 --- /dev/null +++ b/.changeset/spicy-onions-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +- Replaced usage of the deprecated and now removed `rootRoute` and `catalogRouteRef`s from the `catalog-react` package diff --git a/.changeset/strong-suns-hope.md b/.changeset/strong-suns-hope.md new file mode 100644 index 0000000000..876f5f1618 --- /dev/null +++ b/.changeset/strong-suns-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Removing the `EntityName` path for the `useEntityOwnership` as it has never worked correctly. Please pass in an entire `Entity` instead. diff --git a/.changeset/swift-roses-hug.md b/.changeset/swift-roses-hug.md new file mode 100644 index 0000000000..0e6d76ed87 --- /dev/null +++ b/.changeset/swift-roses-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +**BREAKING**: Removing the exports of the raw components that back the `CustomFieldExtensions`. diff --git a/.changeset/swift-rules-hunt.md b/.changeset/swift-rules-hunt.md new file mode 100644 index 0000000000..15707bbf90 --- /dev/null +++ b/.changeset/swift-rules-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +adds passing projectID to the Storage client diff --git a/.changeset/tame-lions-know.md b/.changeset/tame-lions-know.md new file mode 100644 index 0000000000..7f304b22ef --- /dev/null +++ b/.changeset/tame-lions-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +**BREAKING**: Removed the deprecated `GithubAuth.normalizeScopes` method. diff --git a/.changeset/techdocs-cats-whisper.md b/.changeset/techdocs-cats-whisper.md new file mode 100644 index 0000000000..290f8a1d7a --- /dev/null +++ b/.changeset/techdocs-cats-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Remove copyright from old footer in documentation generated with previous version of `mkdocs-techdocs-plugin` (`v0.2.2`). diff --git a/.changeset/techdocs-panthers-listen.md b/.changeset/techdocs-panthers-listen.md new file mode 100644 index 0000000000..0ef28f6281 --- /dev/null +++ b/.changeset/techdocs-panthers-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Show feedback when copying code snippet to clipboard. diff --git a/.changeset/techdocs-toes-lie.md b/.changeset/techdocs-toes-lie.md new file mode 100644 index 0000000000..14ca2567e2 --- /dev/null +++ b/.changeset/techdocs-toes-lie.md @@ -0,0 +1,5 @@ +--- +'@techdocs/cli': patch +--- + +Bump `@backstage/techdocs-common` to `0.11.10` to use `spotify/techdocs:v0.3.7` which upgrades `mkdocs-theme` as a dependency of `mkdocs-techdocs-core`. diff --git a/.changeset/techdocs-war-on-war.md b/.changeset/techdocs-war-on-war.md new file mode 100644 index 0000000000..28722feed3 --- /dev/null +++ b/.changeset/techdocs-war-on-war.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed a bug that could cause searches in the in-context TechDocs search bar to show results from a different TechDocs site. diff --git a/.changeset/tidy-jokes-dream.md b/.changeset/tidy-jokes-dream.md new file mode 100644 index 0000000000..aa9c84fc4b --- /dev/null +++ b/.changeset/tidy-jokes-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +add `userExpand` config option to allow expanding a single relationship diff --git a/.changeset/tiny-jobs-hunt.md b/.changeset/tiny-jobs-hunt.md new file mode 100644 index 0000000000..8cf9db20b8 --- /dev/null +++ b/.changeset/tiny-jobs-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +Modify techdocs builder to automatically append techdocs-core plugin to mkdocs.yaml file if it is missing. Adds an optional configuration item if this plugin needs to be omitted. diff --git a/.changeset/tiny-lobsters-exercise.md b/.changeset/tiny-lobsters-exercise.md new file mode 100644 index 0000000000..78cc753bab --- /dev/null +++ b/.changeset/tiny-lobsters-exercise.md @@ -0,0 +1,29 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +**BREAKING:** +Table column utilities `createNameColumn`, `createOwnerColumn`, `createTypeColumn` as well as actions utilities `createCopyDocsUrlAction` and `createStarEntityAction` are no longer directly exported. Instead accessible through DocsTable and EntityListDocsTable. + +Use as following: + +```tsx +DocsTable.columns.createNameColumn(); +DocsTable.columns.createOwnerColumn(); +DocsTable.columns.createTypeColumn(); + +DocsTable.actions.createCopyDocsUrlAction(); +DocsTable.actions.createStarEntityAction(); +``` + +- Renamed `DocsResultListItem` to `TechDocsSearchResultListItem`, leaving the old name in place as a deprecations. + +- Renamed `TechDocsPage` to `TechDocsReaderPage`, leaving the old name in place as a deprecations. + +- Renamed `TechDocsPageRenderFunction` to `TechDocsPageRenderFunction`, leaving the old name in place as a deprecations. + +- Renamed `TechDocsPageHeader` to `TechDocsReaderPageHeader`, leaving the old name in place as a deprecations. + +- `LegacyTechDocsHome` marked as deprecated and will be deleted in next release, use `TechDocsCustomHome` instead. + +- `LegacyTechDocsPage` marked as deprecated and will be deleted in next release, use `TechDocsReaderPage` instead. diff --git a/.changeset/tough-clocks-enjoy.md b/.changeset/tough-clocks-enjoy.md new file mode 100644 index 0000000000..90e00382df --- /dev/null +++ b/.changeset/tough-clocks-enjoy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Tweaked the wording of the "does not have a location" errors to include the actual missing annotation name, to help users better in fixing their inputs. diff --git a/.changeset/tough-pets-beg.md b/.changeset/tough-pets-beg.md new file mode 100644 index 0000000000..7fdcc6dc01 --- /dev/null +++ b/.changeset/tough-pets-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Fix Bitbucket Cloud and Bitbucket Server line number reference. diff --git a/.changeset/tough-stingrays-sit.md b/.changeset/tough-stingrays-sit.md deleted file mode 100644 index 5cef08c8e3..0000000000 --- a/.changeset/tough-stingrays-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@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/two-forks-tell.md b/.changeset/two-forks-tell.md new file mode 100644 index 0000000000..dbb17a516c --- /dev/null +++ b/.changeset/two-forks-tell.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-org': patch +--- + +Removed usage of deprecated `getEntityMetadataViewUrl` and `getEntityMetadataEditUrl` helpers. diff --git a/.changeset/two-lobsters-hammer.md b/.changeset/two-lobsters-hammer.md new file mode 100644 index 0000000000..412901fb41 --- /dev/null +++ b/.changeset/two-lobsters-hammer.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-catalog-react': patch +--- + +Updated usage of `StorageApi` to use `snapshot` method instead of `get` diff --git a/.changeset/unlucky-queens-brush.md b/.changeset/unlucky-queens-brush.md new file mode 100644 index 0000000000..c080856246 --- /dev/null +++ b/.changeset/unlucky-queens-brush.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Added ability for SidebarSubmenuItem to handle external links correctly via the "to" prop diff --git a/.changeset/weak-news-reply.md b/.changeset/weak-news-reply.md new file mode 100644 index 0000000000..4a7a38cef5 --- /dev/null +++ b/.changeset/weak-news-reply.md @@ -0,0 +1,23 @@ +--- +'@backstage/catalog-model': minor +--- + +**BREAKING**: The following changes are all breaking changes. + +Removed `EDIT_URL_ANNOTATION` and `VIEW_URL_ANNOTATION`, `LOCATION_ANNOTATION`, `ORIGIN_LOCATION_ANNOTATION`, `LOCATION_ANNOTATION`, `SOURCE_LOCATION_ANNOTATION`. All of these constants have been prefixed with ANNOTATION to be easier to find meaning `SOURCE_LOCATION_ANNOTATION` is available as `ANNOTATION_SOURCE_LOCATION`. + +Removed `parseLocationReference`, replaced by `parseLocationRef`. + +Removed `stringifyLocationReference`, replaced by `stringifyLocationRef`. + +Removed `Location` type which has been moved to `catalog-client`. + +Removed `ENTITY_DEFAULT_NAMESPACE`, replaced by `DEFAULT_NAMESPACE`. + +Removed `compareEntityToRef` compare using `stringifyEntityRef` instead. + +Removed `JSONSchema` type which should be imported from `json-schema` package instead. + +Removed utility methods: `entityHasChanges`, `generateEntityEtag`, `generateEntityUid`, `generateUpdatedEntity`. + +Removed `ENTITY_META_GENERATED_FIELDS` and `EntityRefContext`. diff --git a/.changeset/wise-foxes-confess.md b/.changeset/wise-foxes-confess.md new file mode 100644 index 0000000000..8322b66555 --- /dev/null +++ b/.changeset/wise-foxes-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fix the support for custom defaultBranch values for Bitbucket Cloud at the `publish:bitbucket` scaffolder action. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 8303510e77..c7971f6c52 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -3,6 +3,8 @@ accessors addon addons Airbrake +Airbrakes +airbrake Anddddd Apdex api @@ -17,7 +19,7 @@ autoscaling Autoscaling autoselect Avro -backrub +aws backported backporting Bigtable @@ -36,8 +38,6 @@ Changesets chanwit Chanwit ci -cisphobia -cissexist classname cli cloudbuild @@ -65,7 +65,6 @@ css Datadog dataflow dayjs -deadnaming debounce Debounce declaratively @@ -168,7 +167,6 @@ Minikube Minio misconfiguration misconfigured -misgendering mkdocs Mkdocs monorepo @@ -210,6 +208,7 @@ parallelization Patrik Peloton performant +Performant plantuml Platformize Podman @@ -275,6 +274,8 @@ sqlite squidfunk src stdout +stringify +stringified subcomponent subcomponents subkey 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/ci.yml b/.github/workflows/ci.yml index 82bf46a17a..49c97bb197 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,4 @@ name: CI - on: pull_request: paths-ignore: diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index 8b92c000ed..7931f7ade5 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -1,5 +1,4 @@ name: Deploy Microsite - on: push: branches: diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index 3fbc2fafe5..4997668563 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -1,5 +1,4 @@ name: Deploy Nightly Release - on: schedule: - cron: '0 2 * * *' # run at 2 AM UTC diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index ed02a313fe..9b7a233767 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -1,5 +1,4 @@ name: Deploy Packages - on: workflow_dispatch: push: @@ -189,7 +188,7 @@ jobs: run: yarn tsc:full - name: build packages - run: yarn lerna -- run --ignore example-app build + run: yarn lerna -- run --ignore example-app --ignore example-backend build # Publishes current version of packages that are not already present in the registry - name: publish diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 7e0458cb21..75c154792c 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -1,5 +1,4 @@ name: Sync code formatting - on: push: branches: diff --git a/.github/workflows/sync_dependabot-changesets.yml b/.github/workflows/sync_dependabot-changesets.yml index ed97cf05f1..58e0a1ac0f 100644 --- a/.github/workflows/sync_dependabot-changesets.yml +++ b/.github/workflows/sync_dependabot-changesets.yml @@ -2,7 +2,7 @@ name: Sync Dependabot changeset on: pull_request_target: paths: - - .github/workflows/dependabot-changeset-maker.yml + - '.github/workflows/sync_dependabot-changesets.yml' - '**/yarn.lock' jobs: diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index 4b44caafba..f4d9afc0df 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -5,6 +5,7 @@ on: version: description: Version number, without any 'v' prefix required: true + jobs: create-new-version: runs-on: ubuntu-latest diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 93b6c696d0..0bf0340f68 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -1,5 +1,4 @@ name: Sync Snyk GitHub issues - on: schedule: - cron: '0 */4 * * *' diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index f9e6679173..bf50748dc0 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -1,11 +1,10 @@ name: Sync Snyk Monitoring - on: workflow_dispatch: push: branches: [master] paths: - - '.github/workflows/snyk-monitor.yml' + - '.github/workflows/sync_snyk-monitor.yml' - '**/.snyk' - '**/package.json' - 'yarn.lock' diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index e028387190..58d715c5a8 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -1,5 +1,4 @@ name: Sync Version Packages PR - on: push: branches: diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index 57298d69dd..55c1c23587 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -1,9 +1,9 @@ name: Verify Docs Quality - on: pull_request: branches: [master] paths: + - '.github/workflows/verify_docs-quality.yml' - '**.md' jobs: diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 6694ace02b..3e39aee1ae 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -1,5 +1,4 @@ name: E2E Test Linux - on: pull_request: paths-ignore: diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 3806eade25..7caf880366 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -1,5 +1,4 @@ name: E2E Test Techdocs - on: pull_request: paths-ignore: diff --git a/.github/workflows/verify_e2e-tugboat.yml b/.github/workflows/verify_e2e-tugboat.yml index a9c810ff68..1054ccf579 100644 --- a/.github/workflows/verify_e2e-tugboat.yml +++ b/.github/workflows/verify_e2e-tugboat.yml @@ -1,5 +1,6 @@ name: E2E Test Tugboat on: deployment_status + jobs: set-pending: if: github.event.deployment_status.state != 'success' && github.event.deployment_status.state != 'failed' diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index b4347898e6..0977831d19 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -1,11 +1,11 @@ -name: E2E Test Windows - # Building on windows is really slow, so this workflow is separate from e2e.yml and only builds on changes # to the cli itself. They're more likely to introduce issues on windows, compared to changes to core and yarn.lock. + +name: E2E Test Windows on: pull_request: paths: - - '.github/workflows/e2e-win.yml' + - '.github/workflows/verify_e2e-windows.yml' - 'packages/cli/**' - 'packages/e2e-test/**' - 'packages/create-app/**' @@ -20,7 +20,7 @@ jobs: strategy: matrix: - os: [windows-latest] + os: [windows-2019] node-version: [14.x, 16.x] env: diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml index ac05f72be5..24a6ff5351 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -1,9 +1,8 @@ name: Verify Microsite - on: pull_request: paths: - - '.github/workflows/microsite-build-check.yml' + - '.github/workflows/verify_microsite.yml' - 'microsite/**' - 'docs/**' diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 2142f52445..60f58f4c66 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -2,7 +2,7 @@ name: Verify Storybook on: pull_request: paths: - - '.github/workflows/chromatic-storybook-test.yml' + - '.github/workflows/verify_storybook.yml' - 'storybook/**' - 'packages/config/src/**' - 'packages/theme/src/**' diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 55959510d3..9ae4397bba 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -1,13 +1,15 @@ name: Verify Master Branch on Windows - on: workflow_dispatch: push: branches: [master] + pull_request: + paths: + - '.github/workflows/verify_windows.yml' jobs: build: - runs-on: windows-latest + runs-on: windows-2019 strategy: matrix: diff --git a/ADOPTERS.md b/ADOPTERS.md index 531660a877..eb83726b8d 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,3 +1,7 @@ +# Adopters + +_If you're using Backstage in your organization, please try to add your company name to this list. This really helps the project to gain momentum and credibility. It's a small contribution back to the project with a big impact._ + | 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. | @@ -74,7 +78,7 @@ | [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), [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 🥰🚀 | +| [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 | @@ -93,3 +97,6 @@ | [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. | +| [Bradesco](https://banco.bradesco/html/classic/sobre/index.shtm) | [@danilosoarescardoso](https://github.com/danilosoarescardoso) | Centralized developer portal with software catalog, software templates, techdocs and some plugins from community and by our own. | +| [SeatGeek](https://seatgeek.com) | [@zhammer](https://github.com/zhammer) | Software catalog and documentation platform | +| [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f054de05ce..e853a68ad3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Our vision for Backstage is for it to become the trusted standard toolbox (read: UX layer) for the open source infrastructure landscape. Think of it like Kubernetes for developer experience. We realize this is an ambitious goal. We can’t do it alone. -Therefore we want to create strong community of contributors -- all working together to create the kind of delightful experience that our developers deserve. +Therefore we want to create a strong community of contributors -- all working together to create the kind of delightful experience that our developers deserve. Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ❤️ @@ -100,7 +100,7 @@ Signed-off-by: Jane Smith - In case you forgot to add it to the last N commits in your branch, use `git rebase --signoff HEAD~N` and replace N with the number of new commits you created in your branch. - If you have a very deep branch with a lot of commits, run `git rebase -i --signoff $(git merge-base -a master HEAD)`, double check to make sense of the commits (keep all lines as `pick`) and save and close the editor. This should bulk sign all the commits in your PR. Do be careful though. If you have a complex flow with a lot of branching and re-merging of work branches and stuff, merge-base may not be the right solution for you. -Note: If you have already pushed you branch to a remote, you might have to force push: `git push -f` after the rebase. +Note: If you have already pushed your branch to a remote, you might have to force push: `git push -f` after the rebase. ### Using GitHub Desktop? diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 3bcef61021..5bafa19c81 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -24,7 +24,7 @@ To become a maintainer you need to demonstrate the following: - Monitor Discord (delayed response is perfectly acceptable). - Triage GitHub issues and perform pull request reviews for other maintainers and the community. - Triage build issues - file issues for known flaky builds or bugs, and either fix or find someone to fix any master build breakages. -- During GitHub issue triage, apply all applicable ([labels](https://github.com/backstage/backstage/labels)) to each new issue. Labels are extremely useful for future issue follow up. Which labels to apply is somewhat subjective so just use your best judgment. A few of the most important labels that are not self explanatory are: +- During GitHub issue triage, apply all applicable ([labels](https://github.com/backstage/backstage/labels)) to each new issue. Labels are extremely useful for future issue follow-up. Which labels to apply is somewhat subjective so just use your best judgment. A few of the most important labels that are not self-explanatory are: - good first issue: Mark any issue that can reasonably be accomplished by a new contributor with this label. - help wanted: Unless it is immediately obvious that someone is going to work on an issue (and if so assign it), mark it help wanted. - Make sure that ongoing PRs are moving forward at the right pace or closing them. diff --git a/STYLE.md b/STYLE.md index d968d387fc..d74616a88c 100644 --- a/STYLE.md +++ b/STYLE.md @@ -131,7 +131,7 @@ We use [API Extractor](https://api-extractor.com/pages/overview/demo_docs/) to g The doc comments are of the good old `/** ... */` format, with tags of the format `@` used to mark various things. The [TSDoc website](https://tsdoc.org/) has a good index of all available tags. -There are a few things to pay attention to make the documentation show up in a nice way on the website... +There are a few things to pay attention to, in order to make the documentation show up in a nice way on the website... ## Declare exported functions using the `function` keyword @@ -163,7 +163,7 @@ If the parameters of a function are destructed in the parameter list, they will ![image](https://user-images.githubusercontent.com/4984472/133117011-3e6cb6da-40dd-450e-8d33-51bdcf412e08.png) -Instead prefer to use a single parameter variable and then move the destructuring into the function body instead, which will look much nicer: +Instead prefer to use a single parameter variable and then move the destructuring into the function body, which will look much nicer: ![image](https://user-images.githubusercontent.com/4984472/133120542-d648de43-5495-43a8-b532-f5e6f6ab736e.png) diff --git a/app-config.yaml b/app-config.yaml index b025064c74..ab68d4398f 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -211,14 +211,6 @@ catalog: - Location processors: - githubOrg: - providers: - - target: https://github.com - token: ${GITHUB_TOKEN} - #### Example for how to add your GitHub Enterprise instance using the API: - # - target: https://ghe.example.net - # apiBaseUrl: https://ghe.example.net/api - # token: ${GHE_TOKEN} ldapOrg: ### Example for how to add your enterprise LDAP server # providers: diff --git a/contrib/chart/backstage/templates/ingress.yaml b/contrib/chart/backstage/templates/ingress.yaml index 3fa052485e..cac9b61553 100644 --- a/contrib/chart/backstage/templates/ingress.yaml +++ b/contrib/chart/backstage/templates/ingress.yaml @@ -15,7 +15,6 @@ metadata: {{- if .Values.issuer.email }} cert-manager.io/cluster-issuer: {{ .Values.issuer.clusterIssuer }} {{- end }} - kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/ssl-redirect: "false" nginx.ingress.kubernetes.io/configuration-snippet: | if ($scheme = https) { diff --git a/contrib/docker/devops/Dockerfile b/contrib/docker/devops/Dockerfile index 9bb80a0a2c..a7c194618a 100644 --- a/contrib/docker/devops/Dockerfile +++ b/contrib/docker/devops/Dockerfile @@ -9,7 +9,8 @@ WORKDIR /tmp # update package sources # `--virtual` is used to enable removal of these dependencies as a named group -# - python: required by node-gyp +# - python3: required by node-gyp +# - g++: required to build @vscode/sqlite3 # - pixman: required by node-canvas # - pkgconfig: required by pixman # -------------------------------------------- @@ -32,7 +33,8 @@ WORKDIR /tmp RUN apk update \ && apk add --no-cache \ --virtual ${APK_VIRTUAL_NAME} \ - python \ + python3 \ + g++ \ pixman \ pkgconfig \ alpine-sdk \ @@ -51,6 +53,10 @@ RUN wget -qO- ${VALE_INSTALL_URL} \ | tar xvz -C /bin vale \ && chmod +x /bin/vale +# needed for sqlite3 build that needs python +RUN ln -s /usr/bin/python3 /usr/bin/python && \ + ln -s /usr/bin/pip3 /usr/bin/pip + RUN mkdir /app WORKDIR /app diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index 6b1b8cb4e7..a95b276365 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -182,12 +182,12 @@ Use the following `auth` configuration when running Backstage on AWS: auth: providers: awsalb: - issuer: - issuer: https://login.microsoftonline.com/{TENANT_ID}/v2.0 - region: { AWS_REGION } + development: + issuer: https://login.microsoftonline.com//v2.0 + region: ``` -Replace `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App and `{AWS_REGION}` with the AWS region identifier where the ALB is deployed (for example: `eu-central-1`). +Replace `` with the value of `Directory (tenant) ID` of the AAD App and `` with the AWS region identifier where the ALB is deployed (for example: `eu-central-1`). ## Conclusion diff --git a/contrib/search/README.md b/contrib/search/README.md new file mode 100644 index 0000000000..9a43a05c4c --- /dev/null +++ b/contrib/search/README.md @@ -0,0 +1,3 @@ +# Search + +Contributions/extensions to the Search plugin diff --git a/contrib/search/confluence/ConfluenceCollator.md b/contrib/search/confluence/ConfluenceCollator.md new file mode 100644 index 0000000000..f636ae88ea --- /dev/null +++ b/contrib/search/confluence/ConfluenceCollator.md @@ -0,0 +1,95 @@ +ConfluenceCollator.ts reference + +```ts +import { DocumentCollator } from '@backstage/search-common'; +import fetch from 'cross-fetch'; + +export class ConfluenceCollator implements DocumentCollator { + public readonly type: string = 'confluence'; + + async execute() { + const ConfluenceUrlBase = + 'https://{CONFLUENCE-ORG-NAME}.atlassian.net/wiki/rest/api'; + + async function getConfluenceData(requestUrl: string) { + var emptyJson = {}; + try { + const res = await fetch(requestUrl, { + method: 'get', + headers: { + Authorization: `Basic ${process.env.CONFLUENCE_TOKEN}`, + }, + }); + if (res.ok) { + return await res.json(); + } + } catch (err) { + console.error(err); + } + return emptyJson; + } + + async function getSpaces(): Promise { + const data = await getConfluenceData( + `${ConfluenceUrlBase}/space?&limit=1000&type=global&status=current`, + ); + let spacesList = []; + if (data['results']) { + const results = data['results']; + for (const result of results) { + spacesList.push(result['key']); + } + } + return spacesList; + } + + async function getDocumentsFromSpaces(spaces: string[]): Promise { + let documentsList = []; + for (var space of spaces) { + let next = true; + let requestUrl = `${ConfluenceUrlBase}/content?limit=1000&status=current&spaceKey=${space}`; + while (next) { + const data = await getConfluenceData(requestUrl); + if (data['results']) { + const results = data['results']; + for (const result of results) { + documentsList.push(result['_links']['self']); + } + if (data['_links']['next']) { + requestUrl = data['_links']['base'] + data['_links']['next']; + } else { + next = false; + } + } else { + break; + } + } + } + return documentsList; + } + + async function getDocumentInfo(documents: string[]) { + let documentInfo = []; + for (var documentUrl of documents) { + const data = await getConfluenceData( + documentUrl + '?expand=body.storage', + ); + if (data['status'] && data['status'] == 'current') { + const documentMetaData = { + title: data['title'], + text: data['body']['storage']['value'], + location: data['_links']['base'] + data['_links']['webui'], + }; + documentInfo.push(documentMetaData); + } + } + return documentInfo; + } + + const spacesList = await getSpaces(); + const documentsList = await getDocumentsFromSpaces(spacesList); + const documentMetaDataList = await getDocumentInfo(documentsList); + return documentMetaDataList; + } +} +``` diff --git a/contrib/search/confluence/ConfluenceResultListItem.md b/contrib/search/confluence/ConfluenceResultListItem.md new file mode 100644 index 0000000000..ba7c4f91a9 --- /dev/null +++ b/contrib/search/confluence/ConfluenceResultListItem.md @@ -0,0 +1,59 @@ +ConfluenceResultListItem.tsx reference + +```tsx +import React from 'react'; +import { Link } from '@backstage/core-components'; +import { IndexableDocument } from '@backstage/search-common'; +import { + Divider, + ListItem, + ListItemIcon, + ListItemText, +} from '@material-ui/core'; + +type Props = { + result: IndexableDocument; +}; + +export const ConfluenceResultListItem = ({ result }: Props) => { + // Remove html tags from document text before displaying + const chars = []; + let isTag = false; + for (const c of result.text.substring(0, 500)) { + if (c === '<') { + isTag = true; + continue; + } + if (c === '>') { + isTag = false; + chars.push(' '); + continue; + } + if (!isTag) { + chars.push(c); + } + } + const excerpt = + chars.join('').substring(0, 80) + (result.text.length > 80 ? '...' : ''); + + return ( + + + + + + + + + + ); +}; +``` diff --git a/contrib/search/confluence/README.md b/contrib/search/confluence/README.md new file mode 100644 index 0000000000..80beb9c169 --- /dev/null +++ b/contrib/search/confluence/README.md @@ -0,0 +1,32 @@ +# Confluence + +These files help you add Confluence as a source to the Backstage Search plugin. +To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app. +Then, add the following code to your packages/app/src/components/search/SearchPage.tsx: + +```tsx +import { ConfluenceResultListItem } from './ConfluenceResultListItem'; +``` + +```tsx +case 'confluence': + return ( + + ); +``` + +and the following to packages/backend/src/plugins/search.ts: + +```ts +import { ConfluenceCollator } from './search/ConfluenceCollator'; +``` + +```ts +indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + collator: new ConfluenceCollator(), +}); +``` diff --git a/docs/assets/getting-started/sidebar-with-catalog.png b/docs/assets/getting-started/sidebar-with-catalog.png new file mode 100644 index 0000000000..d80346c211 Binary files /dev/null and b/docs/assets/getting-started/sidebar-with-catalog.png differ diff --git a/docs/assets/getting-started/sidebar-without-catalog.png b/docs/assets/getting-started/sidebar-without-catalog.png new file mode 100644 index 0000000000..bfeab13235 Binary files /dev/null and b/docs/assets/getting-started/sidebar-without-catalog.png differ diff --git a/docs/assets/getting-started/simple-homepage.png b/docs/assets/getting-started/simple-homepage.png new file mode 100644 index 0000000000..128707e9ff Binary files /dev/null and b/docs/assets/getting-started/simple-homepage.png differ diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index ee87c2d4e6..c54fb75ffa 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -67,7 +67,7 @@ Bitbucket username to `bitbucket.org/username` annotations of `User` entities in the catalog. Note that you must populate your catalog with matching entities or users will not be able to sign in. -The second resolver is the `bitbucketUsernameSignInResolver`, which works the +The second resolver is the `bitbucketUserIdSignInResolver`, which works the same way, but uses the Bitbucket user ID instead, and matches on the `bitbucket.org/user-id` annotation. diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index a33351e938..3b355470d1 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -27,6 +27,8 @@ sign-in resolvers and set them for any of the Authentication providers inside `@backstage/plugin-auth-backend` plugin. ```ts +import { DEFAULT_NAMESPACE, stringifyEntityRef } from '@backstage/catalog-model'; + export default async function createPlugin({ ... }: PluginEnvironment): Promise { @@ -38,22 +40,31 @@ export default async function createPlugin({ resolver: async ({ profile: { email } }, ctx) => { // Call a custom validator function that checks that the email is // valid and on our own company's domain, and throws an Error if it - // isn't + // isn't. + // TODO: Implement this function validateEmail(email); // List of entity references that denote the identity and // membership of the user - const ent = []; + const ent: string[] = []; // Let's use the username in the email ID as the user's default // unique identifier inside Backstage. const [id] = email.split('@'); - ent.push(`User:default/${id}`) + ent.push(stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + })); // Let's call the internal LDAP provider to get a list of groups // that the user belongs to, and add those to the list as well const ldapGroups = await getLdapGroups(email); - ldapGroups.forEach(group => ent.push(`Group:default/${group}`)) + ldapGroups.forEach(group => ent.push(stringifyEntityRef({ + kind: 'Group', + namespace: DEFAULT_NAMESPACE, + name: group, + }))); // Issue the token containing the entity claims const token = await ctx.tokenIssuer.issueToken({ 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/docker.md b/docs/deployment/docker.md index 6f58abe769..36967f352a 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -56,7 +56,7 @@ Once the host build is complete, we are ready to build our image. The following `Dockerfile` is included when creating a new app with `@backstage/create-app`: ```Dockerfile -FROM node:14-buster-slim +FROM node:16-bullseye-slim WORKDIR /app # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. @@ -65,6 +65,12 @@ WORKDIR /app COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz +# install sqlite3 dependencies +RUN apt-get update && \ + apt-get install -y libsqlite3-dev python3 cmake g++ && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" # Then copy the rest of the backend bundle, along with any other files we might want. @@ -134,23 +140,28 @@ the repo root: ```Dockerfile # Stage 1 - Create yarn install skeleton layer -FROM node:14-buster-slim AS packages +FROM node:16-bullseye-slim AS packages WORKDIR /app COPY package.json yarn.lock ./ COPY packages packages + # Comment this out if you don't have any internal plugins COPY plugins plugins RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+ # Stage 2 - Install dependencies and build packages -FROM node:14-buster-slim AS build +FROM node:16-bullseye-slim AS build WORKDIR /app COPY --from=packages /app . +# install sqlite3 dependencies +RUN apt-get update && apt-get install -y libsqlite3-dev python3 cmake g++ && \ + yarn config set python /usr/bin/python3 + RUN yarn install --frozen-lockfile --network-timeout 600000 && rm -rf "$(yarn cache dir)" COPY . . @@ -159,7 +170,7 @@ RUN yarn tsc RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies # Stage 3 - Build the actual backend image and install production dependencies -FROM node:14-buster-slim +FROM node:16-bullseye-slim WORKDIR /app @@ -167,6 +178,12 @@ WORKDIR /app COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz +# install sqlite3 dependencies +RUN apt-get update && \ + apt-get install -y libsqlite3-dev python3 cmake g++ && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + RUN yarn install --frozen-lockfile --production --network-timeout 600000 && rm -rf "$(yarn cache dir)" # Copy the built packages from the build stage diff --git a/docs/features/search/concepts.md b/docs/features/search/concepts.md index 8cb17b12e6..052f71376f 100644 --- a/docs/features/search/concepts.md +++ b/docs/features/search/concepts.md @@ -54,13 +54,14 @@ An index is a collection of such documents of a given type. ### Collators You need to be able to search something! Collators are the way to define what -can be searched. Specifically, they're classes which return documents conforming -to a minimum set of fields (including a document title, location, and text), but -which can contain any other fields as defined by the collator itself. One -collator is responsible for defining and collecting documents of a type. +can be searched. Specifically, they're readable object streams of documents that +conform to a minimum set of fields (including a document title, location, and +text), but which can contain any other fields as defined by the collator itself. +One collator is responsible for defining and collecting documents of a type. -Some plugins, like the Catalog Backend, provide so-called "default" collators -which you can use out-of-the-box to start searching across Backstage quickly. +Some plugins, like the Catalog Backend, provide so-called "default" collator +factories which you can use out-of-the-box to start searching across Backstage +quickly. ### Decorators @@ -68,9 +69,15 @@ Sometimes you want to add extra information to a set of documents in your search index that the collator may not be aware of. For example, the Software Catalog knows about software entities, but it may not know about their usage or quality. -Decorators are classes which can add extra fields to pre-collated documents. -This extra metadata could then be used to bias search results or otherwise -improve the search experience in your Backstage instance. +Decorators are transform streams which sit between a collator (read stream) and +an indexer (write stream) during the indexing process. It can be used to add +extra fields to documents as they are being collated and indexed. This extra +metadata could then be used to bias search results or otherwise improve the +search experience in your Backstage instance. + +In addition to adding extra metadata, decorators (like any transform stream) can +also be used to remove metadata, filter out, or even add extra documents at +index-time. ### The Scheduler 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/search/how-to-guides.md b/docs/features/search/how-to-guides.md index a3c42c682c..ac36d64673 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -48,10 +48,10 @@ const app = createApp({ ## How to index TechDocs documents The TechDocs plugin has supported integrations to Search, meaning that it -provides a default collator ready to be used. +provides a default collator factory ready to be used. The purpose of this guide is to walk you through how to register the -[DefaultTechDocsCollator](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts) +[DefaultTechDocsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts) in your App, so that you can get TechDocs documents indexed. If you have been through the @@ -60,18 +60,19 @@ you should have the `packages/backend/src/plugins/search.ts` file available. If so, you can go ahead and follow this guide - if not, start by going through the getting started guide. -1. Import the DefaultTechDocsCollator from `@backstage/plugin-techdocs-backend`. +1. Import the `DefaultTechDocsCollatorFactory` from + `@backstage/plugin-techdocs-backend`. ```typescript -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; ``` -2. Register the DefaultTechDocsCollator with the IndexBuilder. +2. Register the `DefaultTechDocsCollatorFactory` with the IndexBuilder. ```typescript indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, logger, tokenManager, @@ -131,3 +132,264 @@ indexBuilder.addCollator({ As shown above, you can add a catalog entity filter to narrow down what catalog entities are indexed by the search engine. + +## How to migrate from Search Alpha to Beta + +For the purposes of this guide, Search Beta version is defined as: + +- **Search Plugin**: At least `v0.x.y` +- **Search Backend Plugin**: At least `v0.x.y` +- **Search Backend Node**: At least `v0.x.y` + +In the Beta version, the Search Platform's indexing process has been rewritten +as a stream pipeline in order to improve efficiency and performance on large +sets of documents. + +If you've not yet extended the Search Platform with custom code, and have +instead taken advantage of default collators, decorators, and search engines +provided by existing plugins, the migration process is fairly straightforward: + +1. Upgrade to at least version `0.x.y` of + `@backstage/plugin-search-backend-node`, as well as any backend plugins whose + collators you are using (e.g. at least version `0.x.y` of + `@backstage/plugin-catalog-backend` and/or version `0.x.y` of + `@backstage/plugin-techdocs-backend`). +2. Then, make the following changes to your + `/packages/backend/src/plugins/search.ts` file: + + ```diff + -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; + -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; + +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; + +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; + // ... + const indexBuilder = new IndexBuilder({ logger, searchEngine }); + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultCatalogCollator.fromConfig(config, { discovery }), + + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }), + }); + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultTechDocsCollator.fromConfig(config, { + + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery, + logger, + }), + }); + ``` + +Any custom collators, decorators, or search engine implementations will require +minor refactoring. Continue on for details. + +### Rewriting alpha-style collators for beta + +In alpha versions of the Backstage Search Platform, collators were classes that +implemented an `execute` method which resolved an `IndexableDocument` array. + +In beta versions, the logic encapsulated by the aforementioned `execute` method +is contained within an [object-mode][obj-mode] `Readable` stream where each +object pushed onto the stream is of type `IndexableDocument`. Instances of this +stream are instantiated by a factory class conforming to the +`DocumentCollatorFactory` interface. + +The optimal conversion strategy will vary depending on the collator's logic, but +the simplest conversion can follow a process like this: + +1. Rename your collator class to something like `YourCollatorFactory` and update + it to implement `DocumentCollatorFactory` instead of `DocumentCollator`. +2. Update its `execute` method so that it resolves + `AsyncGenerator` instead of `YourIndexableDocument[]`. +3. Implement `DocumentCollatorFactory`'s `getCollator` method which resolves to + `Readable.from(this.execute())` (which is a utility for creating [readable + streams][read-stream] from [async generators][async-gen]). + +```ts +import { DocumentCollatorFactory } from '@backstage/plugin-search-backend-node'; +import { Readable } from 'stream'; +export class YourCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'your-type'; + async *execute(): AsyncGenerator { + const widgets = await this.client.getWidgets(); + for (const widget of widgets) { + yield { + title: widget.name, + location: widget.url, + text: widget.description, + }; + } + } + getCollator() { + return Readable.from(this.execute()); + } +} +``` + +Note: it may be possible to simplify your collator dramatically! If your custom +collator was previously using streams under the hood (for example, by reading +newline delimited JSON from a local or remote file), you could just expose the +stream directly via a simple factory class: + +```ts +import { DocumentCollatorFactory } from '@backstage/plugin-search-backend-node'; +import { createReadStream } from 'fs'; +import { parse } from '@jsonlines/core'; +export class YourCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'your-type'; + async getCollator() { + const parseStream = parse(); + return createReadStream('./documents.ndjson').pipe(parseStream); + } +} +``` + +### Rewriting alpha-style decorators for beta + +In alpha versions of the Backstage Search Platform, decorators were classes that +implemented an `execute` method which took an `IndexableDocument` array as an +argument, and resolved a modified array of the same type. + +In beta versions, the logic encapsulated by the aforementioned `execute` method +is contained within an object-mode `Transform` stream which reads objects of +type `IndexableDocument`, and writes objects of a conforming type. Similar to +collators, instances of this stream are instantiated by a factory class +conforming to the `DocumentDecoratorFactory` interface. + +Although you can choose to implement a `Transform` stream from scratch, the +`@backstage/plugin-search-backend-node` package provides a `DecoratorBase` class +in order to simplify the developer experience. With this base class, all that's +needed is to transfer your old decorator class logic into the base class' three +methods (`initialize`, `decorate`, and `finalize`), and implement the factory +class that instantiates the stream: + +```ts +import { DecoratorBase } from '@backstage/plugin-search-backend-node'; +export class YourDecorator extends DecoratorBase { + async initialize() { + // Setup logic. Performed once before any documents are consumed. + } + async decorate( + document: YourIndexableDocument, + ): Promise { + // Perform transformation logic here. + return document; + } + async finalize() { + // Teardown logic. Performed once after all documents have been consumed. + } +} +export class YourDecoratorFactory implements DocumentDecoratorFactory { + async getDecorator() { + return new YourDecorator(); + } +} +``` + +Note the return type of the `decorate` method and how each can be used to +different effect. + +- By resolving a single `YourIndexableDocument` object, your decorator can be + used to make simple transformations: + + ```ts + class BooleanWidgetCoolnessDecorator extends DecoratorBase { + async decorator(widget) { + // Perform a simple, 1:1 transformation. + widget.isCool = widget.isCool === 'true' ? true : false; + return widget; + } + } + ``` + +- By resolving `undefined`, your decorator can filter out documents which + shouldn't be in the index: + + ```ts + class OnlyCoolWidgetsDecorator extends DecoratorBase { + async decorator(widget) { + // Perform a simple filter operation. + return widget.isCool ? widget : undefined; + } + } + ``` + +- By resolving an array of `YourIndexableDocument` objects, you can generate + multiple documents based on the content of one: + + ```ts + class WidgetByVariantDecorator extends DecoratorBase { + async decorator(widget) { + // Generate one widget doc per widget variant. + return widget.variants.map(variant => { + // Each widget doc is the given widget plus a "variant" property + // pulled from a widget.variants string array. + return { + ...widget, + variant, + }; + }); + } + } + ``` + +In alpha versions, a decorator had access to every `IndexableDocument` +simultaneously. This is no longer possible in beta versions (precisely to make +the indexing process more efficient and performant). You will need to modify +your decorator's logic so that it does not need access to every document at +once. + +### Rewriting alpha-style search engines for beta + +Search Engines are responsible for both querying and indexing documents to an +underlying search engine technology. While the search engine query interface +didn't change between alpha and beta versions, the indexing half of the +interface _did_ change. + +In alpha versions of the Backstage Search Platform, a search engine implemented +an `index` method which took a `type` and an `IndexableDocument` array and was +responsible for writing these documents to the underlying search engine. + +In beta versions, the logic encapsulated by the aforementioned `index` method is +contained within an object-mode `Writable` stream which expects objects of type +`IndexableDocument`. On the search engine class itself, the `index` method is +replaced with a `getIndexer` factory method which still takes the `type`, but +resolves an instance of the aforementioned `Writable` stream. + +Although you can choose to implement a `Writable` stream from scratch, the +`@backstage/plugin-search-backend-node` package provides a +`BatchSearchEngineIndexer` class in order to simplify the developer experience. +With this base class, which collects documents in batches of a configurable size +on your behalf, all that's needed is to transfer your old `index` method logic +into the base class' three methods (`initialize`, `index`, and `finalize`), and +implement the factory method that instantiates the stream: + +```ts +import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { SearchEngine } from '@backstage/search-common'; +export class YourSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor({ type }: { type: string }) { + // Customize the number of documents passed to the index method per batch. + super({ batchSize: 500 }); + // An imaginary search engine indexing client. + this.index = new SomeSearchEngineIndex({ indexName: type }); + } + async initialize() { + // Setup logic. Performed once before any documents are consumed. + } + async index(documents: IndexableDocument[]) { + await this.index.batchOf(documents); + } + async finalize() { + // Teardown logic. Performed once after all documents have been consumed. + } +} +export class YourSearchEngine implements SearchEngine { + async getIndexer(type: string) { + return new YourSearchEngineIndexer({ type }); + } +} +``` + +[obj-mode]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_object_mode +[read-stream]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_readable_streams +[async-gen]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#iterating_over_async_generators diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 821f39ff6e..80c9458aee 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -93,7 +93,7 @@ export type CustomFilters = DefaultEntityFilters & { To control this filter, we can create a React component that shows checkboxes for the security tiers. This component will make use of the -`useEntityListProvider` hook, which accepts this extended filter type as a +`useEntityList` hook, which accepts this extended filter type as a [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) parameter: @@ -103,7 +103,7 @@ export const EntitySecurityTierPicker = () => { const { filters: { securityTiers }, updateFilters, - } = useEntityListProvider(); + } = useEntityList(); // Toggles the value, depending on whether it's already selected function onChange(value: string) { diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index b14fd732d7..9f592e7cca 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -57,7 +57,7 @@ The recommended way of instantiating the catalog backend classes is to use the `CatalogBuilder`, as illustrated in the [example backend here](https://github.com/backstage/backstage/blob/master/packages/backend/src/plugins/catalog.ts). We will create a new -[`EntityProvider`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/providers/types.ts) +[`EntityProvider`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/api/provider.ts) subclass that can be added to this catalog builder. Let's make a simple provider that can refresh a set of entities based on a @@ -355,7 +355,7 @@ The recommended way of instantiating the catalog backend classes is to use the `CatalogBuilder`, as illustrated in the [example backend here](https://github.com/backstage/backstage/blob/master/packages/backend/src/plugins/catalog.ts). We will create a new -[`CatalogProcessor`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/ingestion/processors/types.ts) +[`CatalogProcessor`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/api/processor.ts) subclass that can be added to this catalog builder. It is up to you where you put the code for this new processor class. For quick @@ -367,11 +367,11 @@ The class will have this basic structure: ```ts import { UrlReader } from '@backstage/backend-common'; -import { LocationSpec } from '@backstage/catalog-model'; import { results, CatalogProcessor, CatalogProcessorEmit, + LocationSpec, } from '@backstage/plugin-catalog-backend'; // A processor that reads from the fictional System-X @@ -448,13 +448,14 @@ behavior for `system-x` that we implemented earlier. ```ts import { UrlReader } from '@backstage/backend-common'; -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { results, CatalogProcessor, CatalogProcessorEmit, CatalogProcessorCache, CatalogProcessorParser, + LocationSpec, } from '@backstage/plugin-catalog-backend'; // It's recommended to always bump the CACHE_KEY version if you make @@ -472,8 +473,8 @@ type CacheItem = { export class SystemXReaderProcessor implements CatalogProcessor { constructor(private readonly reader: UrlReader) {} - // It's recommended to give the processor a unique name. getProcessorName() { + // The processor name must be unique. return 'system-x-processor'; } diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 8122fefdcd..2a11e1181a 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -123,13 +123,15 @@ Options: --no-pull Do not pull the latest docker image --no-docker Do not use Docker, use MkDocs executable and plugins in current user environment. --techdocs-ref The repository hosting documentation source files e.g. - github:https://ghe.mycompany.net.com/org/repo. + url:https://ghe.mycompany.net.com/org/repo. This value is same as the backstage.io/techdocs-ref annotation of the corresponding Backstage entity. It is completely fine to skip this as it is only being used to set repo_url in mkdocs.yml if not found. --etag A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. + --omitTechdocsCoreMkdocsPlugin An option to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files. + Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file. -v --verbose Enable verbose output. (default: false) -h, --help display help for command ``` diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index 408f092ebd..e34f58bc70 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -46,6 +46,24 @@ between `techdocs-backend` and the storage) [TechDocs Backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend) +## TechDocs Build Strategy + +To accommodate more complex logic surrounding whether or not to build TechDocs, the TechDocs backend +supports selecting a Build Strategy. +The Build Strategy is responsible for deciding whether the documentation requested should be built locally +by the TechDocs backend or not. +Customization of the Build Strategy allows for more complex behaviour regarding whether the TechDocs backend +is responsible for building TechDocs, whether an external process is responsible, or whether a combination +of local builds and an external process is responsible, on an entity-by-entity basis. + +The default Build Strategy results in the TechDocs backend building documentation locally if the +`techdocs.builder` configuration option is set to `'local'`, and skipping any building otherwise. +However any logic that satisfies the Build Strategy interface can be implemented, using the Backstage +config as well as the entity being processed to make a decision. + +For an example of how the Build Strategy can be used to implement a 'hybrid' build model, refer to +the [How to implement a hybrid build strategy](./how-to-guides.md#how-to-implement-a-hybrid-build-strategy) guide. + ## TechDocs Container The TechDocs container is a Docker container available at diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 6317ae365b..3edc55574f 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -37,12 +37,22 @@ techdocs: pullImage: true + mkdocs: + # (Optional) techdocs.generator.omitTechdocsCoreMkdocsPlugin can be used to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files. + # Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file. + omitTechdocsCorePlugin: false + # techdocs.builder can be either 'local' or 'external. - # If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to generate the docs, publish to storage - # and show the generated docs afterwords. This is the "Basic" setup of the TechDocs Architecture. - # If builder is set to 'external', techdocs-backend will only fetch the docs and will NOT try to generate and publish. In this case of 'external', - # we assume that docs are being built by an external process (e.g. in the CI/CD pipeline of the repository). This is the "Recommended" setup of - # the architecture. Read more here https://backstage.io/docs/features/techdocs/architecture + # Using the default build strategy, if builder is set to 'local' and you open a TechDocs page, + # techdocs-backend will try to generate the docs, publish to storage and show the generated docs afterwords. + # This is the "Basic" setup of the TechDocs Architecture. + # Using the default build strategy, if builder is set to 'external' (or anything other than 'local'), techdocs-backend + # will only fetch the docs and will NOT try to generate and publish. + # In this case, we assume that docs are being built by an external process (e.g. in the CI/CD pipeline of the repository). + # This is the "Recommended" setup of the architecture. + # Note that custom build strategies may alter this behaviour. + # Read more about the "Basic" and "Recommended" setups here https://backstage.io/docs/features/techdocs/architecture + # Read more about build strategies here: https://backstage.io/docs/features/techdocs/concepts#techdocs-build-strategy builder: 'local' diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index 383c3ae6d2..f63ff43b7f 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -79,6 +79,9 @@ plugins: - techdocs-core ``` +> Note - The plugins section above is optional. Backstage automatically adds the `techdocs-core` plugin to the +> mkdocs file if it is missing. This functionality can be turned off with a [configuration option](./configuration.md) in Backstage. + Update your component's entity description by adding the following lines to its `catalog-info.yaml` in the root of its repository: diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 863b4b3ecb..56ca2252d0 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -538,3 +538,58 @@ Done! Now you have a support of the following diagrams along with mermaid: - `Vega` - `Vega-Lite` - `WaveDrom` + +## How to implement a hybrid build strategy + +One limitation of the [Recommended deployment](./architecture.md#recommended-deployment) is that +the experience for users requires modifying their CI/CD process to publish +their TechDocs. For some users, this may be unnecessary, and provides a barrier +to entry for onboarding users to Backstage. However, a purely local TechDocs +build restricts TechDocs creators to using the tooling provided in Backstage, +as well as the plugins and features provided in the Backstage-included `mkdocs` +installation. + +To accommodate both of these use-cases, users can implement a custom [Build Strategy](./concepts.md#techdocs-build-strategy) +with logic to encode which TechDocs should be built locally, and which will be +built externally. + +To achieve this hybrid build model: + +1. In your Backstage instance's `app-config.yaml`, set `techdocs.builder` to + `'local'`. This ensures that Backstage will build docs for users who want the + 'out-of-the-box' experience. +2. Configure external storage of TechDocs as normal for a production deployment. + This allows Backstage to publish documentation to your storage, as well as + allowing other users to publish documentation from their CI/CD pipelines. +3. Create a custom build strategy, that implements the `DocsBuildStrategy` interface, + and which implements your custom logic for determining whether to build docs for + a given entity. + For example, to only build docs when an entity has the `company.com/techdocs-builder` + annotation set to `'local'`: + + ```typescript + export class AnnotationBasedBuildStrategy { + private readonly config: Config; + + constructor(config: Config) { + this.config = config; + } + + async shouldBuild(_: Entity): Promise { + return ( + this.entity.metadata?.annotations?.['company.com/techdocs-builder'] === + 'local' + ); + } + } + ``` + +4. Pass an instance of this Build Strategy as the `docsBuildStrategy` parameter of the + TechDocs backend `createRouter` method. + +Users should now be able to choose to have their documentation built and published by +the TechDocs backend by adding the `company.com/techdocs-builder` annotation to their +entity. If the value of this annotation is `'local'`, the TechDocs backend will build +and publish the documentation for them. If the value of the `company.com/techdocs-builder` +annotation is anything other than `'local'`, the user is responsible for publishing +documentation to the appropriate location in the TechDocs external storage. diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index fc68008ae8..34eca2429a 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -257,68 +257,4 @@ const LogoFull = () => { ## Custom Homepage In addition to a custom theme, a custom logo, you can also customize the -homepage of your app. To do that we need to go through a few steps. - -### Setting up the Home Page - -1. Create a Home Page Component that will be used for composition. - -`packages/app/src/components/home/HomePage.tsx` - -```tsx -import React from 'react'; - -export const HomePage = () => { - return { - /* TODO: Compose a Home Page here */ - }; -}; -``` - -2. Add a route where the homepage will live, presumably `/`. - -`packages/app/src/App.tsx` - -```tsx -import { HomepageCompositionRoot } from '@backstage/plugin-home'; -import { HomePage } from './components/home/HomePage'; - -// ... -}> - -; -// ... -``` - -### Composing your Home Page - -Composing a Home Page is no different from creating a regular React Component, -i.e. the App Integrator is free to include whatever content they like. However, -there are components developed with the Home Page in mind. If you are looking -for components to use when composing your homepage, you can take a look at the -[collection of Homepage components](https://backstage.io/?path=/story/plugins-home-components) -in storybook. If you don't find a component that suits your needs but want to -contribute, check the -[Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing). - -> If you want to use one of the available homepage templates you can find the -> [templates](https://backstage.io/storybook/?path=/story/plugins-home-templates) -> in the storybook under the "Home" plugin. And if you would like to contribute -> a template, please see the -> [Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing) - -```tsx -import React from 'react'; -import Grid from '@material-ui/core/Grid'; -import { HomePageCompanyLogo } from '@backstage/plugin-home'; - -export const HomePage = () => { - return ( - - - - - - ); -}; -``` +homepage of your app. Read the full guide on the [next page](homepage.md). diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 55285e7f4e..082fbf5b70 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -28,7 +28,7 @@ At the end of this tutorial, you can expect: These instructions can be skipped if you already have a PostgreSQL server installed and created a schema and user. The example below is for Linux, but -luckily there's detailed instructions on how to +luckily there are detailed instructions on how to [install PostgreSQL](https://www.postgresql.org/download/) to help you get started. @@ -113,7 +113,7 @@ new component, or register an existing one it will be saved in the database. Later in this tutorial you'll add a service, and you can test if it's persistent as advertised. -If you want to read more about the database configuration, here's some helpful +If you want to read more about the database configuration, here are some helpful links: - [Configuring Plugin Databases](../tutorials/configuring-plugin-databases.md#privileges) @@ -122,7 +122,7 @@ links: ### Setting up authentication -There's multiple authentication providers available for you to use with +There are multiple authentication providers available for you to use with Backstage, feel free to follow [the instructions for adding authentication](../auth/). @@ -191,7 +191,7 @@ components: { That should be it. You can stop your Backstage App. When you start it again and go to your Backstage portal in your browser, you should have your login prompt! -To learn more about Authentication in Backstage, there's the following docs you +To learn more about Authentication in Backstage, here are some docs you could read: - [Adding Authentication](../auth/) @@ -208,7 +208,7 @@ and Groups can also be loaded from an organization. While using GitHub Apps might be the best way to set up integrations, for this tutorial you'll use a Personal Access Token. -Create your Personal Access Token by opening the +Create your Personal Access Token by opening [the GitHub token creation page](https://github.com/settings/tokens/new). Use a name to identify this token and put it in the notes field. Choose a number of days for expiration. If you have a hard time picking a number, we suggest to go diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index 1d65f25d1b..05c12ffb75 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -16,8 +16,7 @@ may want to [Run Backstage Locally](./running-backstage-locally.md) instead. ## Create an app To create a Backstage app, you will need to have -[Node.js](https://nodejs.org/en/download/) Active LTS Release installed -(currently v14). +Node.js [Active LTS Release](https://nodejs.org/en/about/releases/) installed. Backstage provides a utility for creating new apps. It guides you through the initial setup of selecting the name of the app and a database for the backend. diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md new file mode 100644 index 0000000000..59272a285b --- /dev/null +++ b/docs/getting-started/homepage.md @@ -0,0 +1,166 @@ +--- +id: homepage +title: Backstage homepage - Setup and Customization +description: Documentation on setting up and customizing Backstage homepage +--- + +## Homepage + +Having a good Backstage homepage can significantly improve the discoverability of the platform. You want your users to find all the things they need right from the homepage and never have to remember direct URLs in Backstage. The [Home plugin](https://github.com/backstage/backstage/tree/master/plugins/home) introduces a system for composing a homepage for Backstage in order to surface relevant info and provide convenient shortcuts for common tasks. It's designed with composability in mind with an open ecosystem that allows anyone to contribute with any component, to be included in any homepage. + +For App Integrators, the system is designed to be composable to give total freedom in designing a Homepage that suits the needs of the organization. From the perspective of a Component Developer who wishes to contribute with building blocks to be included in Homepages, there's a convenient interface for bundling the different parts and exporting them with both error boundary and lazy loading handled under the surface. + +At the end of this tutorial, you can expect: + +- Your Backstage app to have a dedicated homepage instead of Software Catalog. +- Understand the composability of homepage and how to start customizing it for your own organization. + +### Prerequisites + +Before we begin, make sure + +- You have created your own standalone Backstage app using [`@backstage/create-app`](index.md#create-your-backstage-app) and not using a fork of the [backstage](https://github.com/backstage/backstage) repository. +- You do not have an existing homepage, and by default you are redirected to Software Catalog when you open Backstage. + +Now, let's get started by installing the home plugin and creating a simple homepage for your Backstage app. + +### Setup homepage + +#### 1. Install the plugin + +``` +# From your Backstage application directory +cd packages/app +yarn add @backstage/plugin-home +``` + +#### 2. Create a new HomePage component + +Inside your `packages/app` directory, create a new file where our new homepage component is going to live. Create `packages/app/src/components/home/HomePage.tsx` with the following initial code + +```tsx +import React from 'react'; + +export const HomePage = () => { + /* We will shortly compose a pretty homepage here. */ + return

Welcome to Backstage!

; +}; +``` + +#### 3. Update router for the root `/` route + +If you don't have a homepage already, most likely you have a redirect setup to use the Catalog homepage as a homepage. + +Inside your `packages/app/src/App.tsx`, look for + +```tsx +const routes = ( + + +``` + +Let's replace the `` line and use the new component we created in the previous step as the new homepage. + +```diff +// File: packages/app/src/App.tsx + ++ import { HomepageCompositionRoot } from '@backstage/plugin-home'; ++ import { HomePage } from './components/home/HomePage'; + +// ... +const routes = ( + +- ++ }> ++ ++ +// ... + +) +``` + +### 4. Update sidebar items + +Let's update the route for "Home" in the Backstage sidebar to point to the new homepage. We'll also add a Sidebar item to quickly open Catalog. + + + + + + + + +
Sidebar without Catalog + Sidebar with Catalog +
BeforeAfter
+ +The code for the Backstage sidebar is most likely inside your [`packages/app/src/components/Root/Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx). + +Let's make the following changes + +```diff +// Other imports ++ import CategoryIcon from '@material-ui/icons/Category'; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + + # ... + }> + {/* Global nav, not org-specific */} +- ++ ++ + + + + + {/* End global nav */} + +``` + +That's it! You should now have _(although slightly boring)_ a homepage! + +Screenshot of a blank homepage + +In the next steps, we will make it interesting and useful! + +### Use the default template + +There is a default homepage template ([storybook link](https://backstage.io/storybook/?path=/story/plugins-home-templates--default-template)) which we will use to set up our homepage. Checkout the [blog post announcement](https://backstage.io/blog/2022/01/25/backstage-homepage-templates) about the Backstage homepage templates for more information. + + + +### Composing your homepage + +Composing a homepage is no different from creating a regular React Component, +i.e. the App Integrator is free to include whatever content they like. However, +there are components developed with the homepage in mind. If you are looking +for components to use when composing your homepage, you can take a look at the +[collection of Homepage components](https://backstage.io/storybook?path=/story/plugins-home-components) +in storybook. If you don't find a component that suits your needs but want to +contribute, check the +[Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing). + +> If you want to use one of the available homepage templates you can find the +> [templates](https://backstage.io/storybook/?path=/story/plugins-home-templates) +> in the storybook under the "Home" plugin. And if you would like to contribute +> a template, please see the +> [Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing) + +```tsx +import React from 'react'; +import Grid from '@material-ui/core/Grid'; +import { HomePageCompanyLogo } from '@backstage/plugin-home'; + +export const HomePage = () => { + return ( + + + + + + ); +}; +``` diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index a622313739..8c2f6bc865 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -25,7 +25,7 @@ guide to do a repository-based installation. [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) - An account with elevated rights to install the dependencies - `curl` or `wget` installed -- Node.js Active LTS Release installed (currently v14) using one of these +- Node.js [Active LTS Release](https://nodejs.org/en/about/releases/) installed using one of these methods: - Using `nvm` (recommended) - [Installing nvm](https://github.com/nvm-sh/nvm#install--update-script) diff --git a/docs/getting-started/running-backstage-locally.md b/docs/getting-started/running-backstage-locally.md index daad7bf195..3443b85285 100644 --- a/docs/getting-started/running-backstage-locally.md +++ b/docs/getting-started/running-backstage-locally.md @@ -13,13 +13,14 @@ trying to contribute, follow the instructions to - Node.js -First make sure you are using Node.js with an Active LTS Release, currently v14. +First make sure you are using Node.js with an [Active LTS Release](https://nodejs.org/en/about/releases/). This is made easy with a version manager such as [nvm](https://github.com/nvm-sh/nvm) which allows for version switching. ```bash -# Installing a new version -nvm install 14 +# Installing current LTS release +nvm install --lts +> Installing latest LTS version. > Downloading and installing node v14.15.1... > Now using node v14.15.1 (npm v6.14.8) diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index ad452f47af..1deec423a7 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -12,7 +12,7 @@ DevOps organization and register entities matching the configured path. This can be useful as an alternative to static locations or manually adding things to the catalog. -To use the discovery processor, you'll need a GitHub integration +To use the discovery processor, you'll need a Azure integration [set up](locations.md) with a `AZURE_TOKEN`. Then you can add a location target to the catalog configuration: diff --git a/docs/integrations/github/locations.md b/docs/integrations/github/locations.md index 330b03eab9..3395c61117 100644 --- a/docs/integrations/github/locations.md +++ b/docs/integrations/github/locations.md @@ -57,6 +57,22 @@ public GitHub, for which we can infer them). The `apiBaseUrl` will always be preferred over the other if a `token` is given, otherwise `rawBaseUrl` will be preferred. +### Token scopes + +When creating a personal access token on GitHub, you must select scopes to +define the level of access for the token. The scopes required vary depending on +your use of the integration: + +- Reading software components: + - `repo` +- Reading organization data: + - `read:org` + - `read:user` + - `user:email` +- Publishing software templates: + - `repo` + - `workflow` (if templates include GitHub workflows) + ## Authentication with GitHub Apps Alternatively, Backstage can use GitHub Apps for backend authentication. This diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 7140ffa505..5843944e8a 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -22,19 +22,6 @@ entities that mirror your org setup. The processor that performs the import, `GithubOrgReaderProcessor`, comes installed with the default setup of Backstage. -If you have _replaced_ the set of processors in your installation using -`replaceProcessors`, you can import and add it as follows: - -```ts -// Typically in packages/backend/src/plugins/catalog.ts -import { GithubOrgReaderProcessor } from '@backstage/plugin-catalog-backend'; - -builder.replaceProcessors( - GithubOrgReaderProcessor.fromConfig(config, { logger }), - // ... -); -``` - ## Configuration The following configuration enables an import of the teams and users under the @@ -51,10 +38,12 @@ If Backstage is configured to use GitHub Apps authentication you must grant `Read-Only` access for `Members` under `Organization` in order to ingest users correctly. You can modify the app's permissions under the organization settings, `https://github.com/organizations/{ORG}/settings/apps/{APP_NAME}/permissions`. + ![permissions](../../assets/integrations/github/permissions.png) **Please note that when you change permissions, the app owner will get an email that must be approved first before the changes are applied.** + ![email](../../assets/integrations/github/email.png) Locations point out the specific org(s) you want to import. The `type` of these @@ -65,3 +54,22 @@ The authorization for loading org information comes from a configured [GitHub integration](locations.md#configuration). When using a personal access token, the token needs to have at least the scopes `read:org`, `read:user`, and `user:email` in the given `target`. + +## Addendum + +Some Backstage apps have _replaced_ the catalog processors to override the +default catalog processing behavior. These apps will already have a call to +`replaceProcessors` in the catalog initialization. + +In the unlikely scenario that this applies to your app, you can import and +add the `GithubOrgReaderProcessor` as follows: + +```ts +// Typically in packages/backend/src/plugins/catalog.ts +import { GithubOrgReaderProcessor } from '@backstage/plugin-catalog-backend'; + +builder.replaceProcessors( + // ... other processor replacements + GithubOrgReaderProcessor.fromConfig(config, { logger }), +); +``` diff --git a/docs/local-dev/cli-build-system.md b/docs/local-dev/cli-build-system.md index fdfede6e05..f57efc1255 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/local-dev/cli-build-system.md @@ -334,13 +334,19 @@ The following is an example of a `Dockerfile` that can be used to package the output of `backstage-cli backend:bundle` into an image: ```Dockerfile -FROM node:14-buster-slim +FROM node:16-bullseye-slim WORKDIR /app COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz -RUN yarn install --production --frozen-lockfile --network-timeout 300000 && rm -rf "$(yarn cache dir)" +# install sqlite3 dependencies +RUN apt-get update && \ + apt-get install -y libsqlite3-dev python3 cmake g++ && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + +RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" COPY packages/backend/dist/bundle.tar.gz app-config.yaml ./ RUN tar xzf bundle.tar.gz && rm bundle.tar.gz @@ -444,8 +450,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 +471,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 @@ -506,3 +552,36 @@ The following is an excerpt of a typical setup of an isomorphic library package: }, "files": ["dist"], ``` + +## Experimental Type Build + +The Backstage CLI has an experimental feature where multiple different type definition files can be generated for different release stages. The release stages are marked in the [TSDoc](https://tsdoc.org/) for each individual export, using either `@public`, `@alpha`, or `@beta`. Rather than just building a single `index.d.ts` file, the build process will instead output `index.d.ts`, `index.beta.d.ts`, and `index.alpha.d.ts`. Each of these files will have exports from more unstable release stages stripped, meaning that `index.d.ts` will omit all exports marked with `@alpha` or `@beta`, while `index.beta.d.ts` will omit all exports marked with `@alpha`. + +This feature is aimed at projects that publish to package registries and wish to maintain different levels of API stability within each package. There is no need to use this within a single monorepo, as it has no effect due to only applying to built and published packages. + +In order for the experimental type build to work, `@microsoft/api-extractor` must be installed in your project, as it is an optional peer dependency of the Backstage CLI. There are then three steps that need to be taken for each package where you want to enable this feature: + +- Add the `--experimental-type-build` flag to the `"build"` script of the package. +- Add either one or both of `"alphaTypes"` and `"betaTypes"` to the `"publishConfig"` of the package: + ```json + "publishConfig": { + ... + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts", + "betaTypes": "dist/index.beta.d.ts" + }, + ``` +- Add either one or both of `"alpha"` and `"beta"` to the `"files"` of the package: + ```json + "files": [ + "dist", + "alpha", + "beta" + ] + ``` + +Once this setup is complete, users of the published packages will only be able to access the stable API via the main package entry point, for example `@acme/my-plugin`. Exports marked with `@alpha` or `@beta` will only be available via the `/alpha` entry point, for example `@acme/my-plugin/alpha`, and exports marked with `@beta` will only be available via `/beta`. This does not apply within the monorepo that contains the package. There all exports still have to be imported via the main entry point. + +Note that these different entry points are only separated during type checking. At runtime they all share the same code which contains the exports from all releases stages. + +An example of this setup can be seen in the [`@backstage/catalog-model`](https://github.com/backstage/backstage/blob/da0675bf9f28ed1460f03635a22d3c26abd14707/packages/catalog-model/package.json#L14) package, which has enabled `alpha` type exports. With this setup, exports marked as `@alpha` are only available for import via `@backstage/catalog-model/alpha`. The `@backstage/catalog-model` package currently does not have any exports marked as `@beta`, or a `/beta` entry point. 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/overview/adopting.md b/docs/overview/adopting.md index 152be21a2c..0765d4a452 100644 --- a/docs/overview/adopting.md +++ b/docs/overview/adopting.md @@ -25,7 +25,7 @@ This team will have **four** primary objectives: 2. Drive adoption of customers (developers at your company). -3. Work with senior tech leadership and architects to ensure your organizations +3. Work with senior tech leadership and architects to ensure your organization's best practices for software development are encoded into a set of [Software Templates](../features/software-templates/index.md). @@ -62,7 +62,7 @@ products they maintain. ### Tactics -Example of tactics we have used to evangelize Backstage internally: +Examples of tactics we have used to evangelize Backstage internally: - Arrange "Lunch & Learns" and seminars. Frequently offer teams interested in Backstage development to come to a seminar where you show, for example, how to @@ -77,13 +77,13 @@ Example of tactics we have used to evangelize Backstage internally: - Show & tell meetings. In order to build an internal community around Backstage we have quarterly meetings where anyone working on Backstage is invited to - present their work. This is a not only a great way to get early feedback, but + present their work. This is not only a great way to get early feedback, but also helps coordination between teams that are building overlapping experiences. - Provide metrics. Add instrumentation to your Backstage deployment and make - metrics available to contributing teams. At Spotify we have even gone so far - as sending out weekly digest email showing how usage metrics have changed for + metrics available to contributing teams. At Spotify, we have even gone so far + as to send out weekly digest emails showing how usage metrics have changed for individual plugins. - Pro-actively identify new plugins. Reach out to teams that own internal UIs or @@ -109,12 +109,12 @@ successful impact on your software development process: - **Deploys to production** Cousin to the metric above: How many times does an engineer push changes into production. -- **MTTR** With clear ownership of all the pieces in your micro services +- **MTTR** With clear ownership of all the pieces in your microservices ecosystem and all tools integrated into one place, Backstage makes it quicker for teams to find the root cause of failures, and fix them. - **Context switching** Reducing context switching can help engineers stay in - the "zone". We measure the number of different tools an engineer have to + the "zone". We measure the number of different tools an engineer has to interact with in order to get a certain job done (e.g. push a change, follow it into production and validate it did not break anything). diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 5d27cc4184..5b6b81cf10 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -281,11 +281,11 @@ stores as a means of improving performance or reliability. Similar to how databases are supported, plugins receive logically separated cache connections, which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood. -At this time of writing, Backstage can be configured to use one of two cache -stores: memory, which is mainly used for local testing, and memcache, which is a -cache store better suited for production deployment. The right cache store for -your Backstage instance will depend on your own run-time constraints and those -required of the plugins you're running. +At this time of writing, Backstage can be configured to use one of three cache +stores: memory, which is mainly used for local testing, memcache or Redis, +which are cache stores better suited for production deployment. The right cache +store for your Backstage instance will depend on your own run-time constraints +and those required of the plugins you're running. ### Use memory for cache @@ -304,6 +304,15 @@ backend: connection: user:pass@cache.example.com:11211 ``` +### Use Redis for cache + +```yaml +backend: + cache: + store: redis + connection: redis://user:pass@cache.example.com:6379 +``` + Contributions supporting other cache stores are welcome! ## Containerization diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index 1c620348bb..8ecd7c4a38 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -21,7 +21,7 @@ mean features in the radar, but not yet scheduled. The long-term roadmap (12 - 36 months) is not detailed in the public roadmap. Third-party contributions are also not currently included in the roadmap. Let us -know about any ongoing developments and we’re happy to include it here as well. +know about any ongoing developments and we’re happy to include them here as well. ## What’s next diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md index 1be1502b6b..434c9a4882 100644 --- a/docs/plugins/github-apps.md +++ b/docs/plugins/github-apps.md @@ -109,11 +109,21 @@ privateKey: | This will result in backstage preventing the use of any installation that is not within the allow list. -### Permissions for pull requests +### App permissions -These are the minimum permissions required for creating a pull request with -Backstage software templates: +When creating a GitHub App, you must select permissions to define the level of +access for the app. The permissions required vary depending on your use of the +integration: -- Read and Write permissions for `Contents`. -- Read and write permissions for `Pull Requests` and `Issues`. -- Read permissions on `Metadata`. +- Reading software components: + - `Contents`: `Read-only` +- Reading organization data: + - `Members`: `Read-only` +- Publishing software templates: + + - `Administration`: `Read & write` (for creating repositories) + - `Contents`: `Read & write` + - `Metadata`: `Read-only` + - `Pull requests`: `Read & write` + - `Issues`: `Read & write` + - `Workflows`: `Read & write` (if templates include GitHub workflows) diff --git a/docs/publishing.md b/docs/publishing.md index 2951f7d30b..a0d67b7d24 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -65,14 +65,5 @@ process is used to release an emergency fix as version `6.5.1` in the patch rele master branch as well. Create a PR that contains the following: - [ ] 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": "6.5.1" - } - } - ``` + - [ ] An updated `CHANGELOG.md` of all patched packages from the tip of the patch branch, `git checkout origin/patch/v1.18.0 -- {packages,plugins}/*/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." diff --git a/microsite/blog/2022-01-25-backstage-homepage-templates.md b/microsite/blog/2022-01-25-backstage-homepage-templates.md index 7ed5ee92e3..8ca8b7373e 100644 --- a/microsite/blog/2022-01-25-backstage-homepage-templates.md +++ b/microsite/blog/2022-01-25-backstage-homepage-templates.md @@ -33,7 +33,7 @@ _Example of the default homepage template in Backstage Storybook_ ## How to get started -To get started, you first need to set up your homepage plugin in the app if you have not already, then you can move on to composing your homepage. You can find our step-by-step documentation of how to do this under the [App Configuration - Customize the look-and-feel of your App documentation](https://backstage.io/docs/getting-started/app-custom-theme#custom-homepage). You can also access the [Backstage UI Kit in Figma](https://www.figma.com/file/nUaAw56hTgC0RIOYkuLSrB/Backstage-Design-System?node-id=2185%3A2978) to duplicate your own version and play around with a fitting homepage for your own organization. +To get started, you first need to set up your homepage plugin in the app if you have not already, then you can move on to composing your homepage. You can find our step-by-step documentation of how to do this under the [App Configuration - Customize the look-and-feel of your App documentation](https://backstage.io/docs/getting-started/homepage). You can also access the [Backstage UI Kit in Figma](https://www.figma.com/file/nUaAw56hTgC0RIOYkuLSrB/Backstage-Design-System?node-id=2185%3A2978) to duplicate your own version and play around with a fitting homepage for your own organization. ![Example of homepage components in the Backstage UI Kit in Figma](assets/22-01-25/homepage-components-figma.png) _Example of the homepage components in the Backstage UI Kit in Figma_ diff --git a/microsite/data/plugins/airbrake.yaml b/microsite/data/plugins/airbrake.yaml index 96de75c002..29c1f394d7 100644 --- a/microsite/data/plugins/airbrake.yaml +++ b/microsite/data/plugins/airbrake.yaml @@ -4,6 +4,6 @@ author: Simply Business authorUrl: https://github.com/simplybusiness/ category: Monitoring description: Access Airbrake error monitoring and other integrations from within Backstage -documentation: https://github.com/backstage/backstage/blob/master/plugins/airbrake/README.md +documentation: https://github.com/backstage/backstage/blob/master/plugins/airbrake iconUrl: https://wp-assets.airbrake.io/wp-content/uploads/2020/10/05222904/Square-white-A-on-Orange.png npmPackageName: '@backstage/plugin-airbrake' 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/microsite/sidebars.json b/microsite/sidebars.json index cb7c0d5036..5369182013 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -22,7 +22,8 @@ "label": "App configuration", "ids": [ "getting-started/configure-app-with-plugins", - "getting-started/app-custom-theme" + "getting-started/app-custom-theme", + "getting-started/homepage" ] }, "getting-started/keeping-backstage-updated", diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 01226a9bf9..a9161f57fd 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -5215,9 +5215,9 @@ prettier@^2.5.1: integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== prismjs@^1.22.0: - version "1.25.0" - resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" - integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== + version "1.27.0" + resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" + integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== process-nextick-args@~2.0.0: version "2.0.1" @@ -6499,9 +6499,9 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.4.3: - version "1.5.3" - resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" - integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== + version "1.5.10" + resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" diff --git a/package.json b/package.json index 242c64c8bb..1882d400b4 100644 --- a/package.json +++ b/package.json @@ -45,11 +45,9 @@ ] }, "resolutions": { - "**/@graphql-codegen/cli/**/ws": "^7.4.6", - "**/@roadiehq/**/@backstage/plugin-catalog": "*", - "**/@roadiehq/**/@backstage/catalog-model": "*" + "**/@graphql-codegen/cli/**/ws": "^7.4.6" }, - "version": "0.67.0", + "version": "0.69.0", "dependencies": { "@manypkg/get-packages": "^1.1.3", "@microsoft/api-documenter": "^7.15.0", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 74313008cd..6fb6c46b76 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/app-defaults +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + - @backstage/core-app-api@0.5.4 + - @backstage/plugin-permission-react@0.3.2 + +## 0.1.8 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-app-api@0.5.3 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-permission-react@0.3.1 + - @backstage/theme@0.2.15 + ## 0.1.7 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index c7cb87faa4..e65d8f53ac 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,13 +1,16 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "0.1.7", + "version": "0.1.9", "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,19 +24,20 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli build --outputs types,esm", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@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", + "@backstage/core-components": "^0.8.10", + "@backstage/core-app-api": "^0.5.4", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "react-router-dom": "6.0.0-beta.0" @@ -42,8 +46,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7", diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index f26c298fb1..3f5cfc1c58 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -21,16 +21,13 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, - OAuth2, OktaAuth, GitlabAuth, - Auth0Auth, MicrosoftAuth, BitbucketAuth, OAuthRequestManager, WebStorage, UrlPatternDiscovery, - SamlAuth, OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, @@ -49,16 +46,12 @@ import { oauthRequestApiRef, googleAuthApiRef, githubAuthApiRef, - oauth2ApiRef, oktaAuthApiRef, gitlabAuthApiRef, - auth0AuthApiRef, microsoftAuthApiRef, storageApiRef, configApiRef, - samlAuthApiRef, oneloginAuthApiRef, - oidcAuthApiRef, bitbucketAuthApiRef, atlassianAuthApiRef, } from '@backstage/core-plugin-api'; @@ -197,46 +190,6 @@ export const apis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: auth0AuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oauth2ApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: samlAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => - SamlAuth.create({ - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: oneloginAuthApiRef, deps: { @@ -251,25 +204,6 @@ export const apis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: oidcAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'oidc', - title: 'Your Identity Provider', - icon: () => null, - }, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: bitbucketAuthApiRef, deps: { diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 1139bf2cbb..029b97ec30 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,110 @@ # example-app +## 0.2.66 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@0.9.0 + - @backstage/cli@0.14.1 + - @backstage/core-components@0.8.10 + - @backstage/plugin-api-docs@0.8.0 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/plugin-catalog-graph@0.2.12 + - @backstage/plugin-jenkins@0.6.0 + - @backstage/plugin-scaffolder@0.13.0 + - @backstage/plugin-techdocs@0.14.0 + - @backstage/plugin-airbrake@0.3.0 + - @backstage/catalog-model@0.11.0 + - @backstage/plugin-catalog-import@0.8.3 + - @backstage/plugin-explore@0.3.31 + - @backstage/core-plugin-api@0.7.0 + - @backstage/plugin-circleci@0.3.0 + - @backstage/plugin-cloudbuild@0.3.0 + - @backstage/plugin-github-actions@0.5.0 + - @backstage/plugin-kafka@0.3.0 + - @backstage/plugin-kubernetes@0.6.0 + - @backstage/plugin-lighthouse@0.3.0 + - @backstage/plugin-org@0.5.0 + - @backstage/plugin-rollbar@0.4.0 + - @backstage/plugin-gocd@0.1.6 + - @backstage/plugin-azure-devops@0.1.16 + - @backstage/plugin-badges@0.2.24 + - @backstage/plugin-code-coverage@0.1.27 + - @backstage/plugin-home@0.4.16 + - @backstage/plugin-newrelic-dashboard@0.1.8 + - @backstage/plugin-pagerduty@0.3.27 + - @backstage/plugin-search@0.7.1 + - @backstage/plugin-sentry@0.3.38 + - @backstage/plugin-tech-insights@0.1.10 + - @backstage/plugin-todo@0.2.2 + - @backstage/plugin-cost-insights@0.11.22 + - @backstage/app-defaults@0.1.9 + - @backstage/core-app-api@0.5.4 + - @backstage/integration-react@0.1.23 + - @backstage/plugin-apache-airflow@0.1.8 + - @backstage/plugin-gcp-projects@0.3.19 + - @backstage/plugin-graphiql@0.2.32 + - @backstage/plugin-newrelic@0.3.18 + - @backstage/plugin-permission-react@0.3.2 + - @backstage/plugin-shortcuts@0.2.1 + - @backstage/plugin-tech-radar@0.5.7 + - @backstage/plugin-user-settings@0.3.21 + +## 0.2.65 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.14.0 + - @backstage/core-app-api@0.5.3 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/integration-react@0.1.22 + - @backstage/plugin-airbrake@0.2.0 + - @backstage/plugin-apache-airflow@0.1.7 + - @backstage/plugin-api-docs@0.7.3 + - @backstage/plugin-azure-devops@0.1.15 + - @backstage/plugin-badges@0.2.23 + - @backstage/plugin-catalog@0.8.0 + - @backstage/plugin-catalog-import@0.8.2 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/plugin-circleci@0.2.38 + - @backstage/plugin-cloudbuild@0.2.36 + - @backstage/plugin-code-coverage@0.1.26 + - @backstage/plugin-cost-insights@0.11.21 + - @backstage/plugin-explore@0.3.30 + - @backstage/plugin-gcp-projects@0.3.18 + - @backstage/plugin-github-actions@0.4.36 + - @backstage/plugin-gocd@0.1.5 + - @backstage/plugin-graphiql@0.2.31 + - @backstage/plugin-home@0.4.15 + - @backstage/plugin-jenkins@0.5.21 + - @backstage/plugin-kafka@0.2.29 + - @backstage/plugin-kubernetes@0.5.8 + - @backstage/plugin-lighthouse@0.2.38 + - @backstage/plugin-newrelic@0.3.17 + - @backstage/plugin-newrelic-dashboard@0.1.7 + - @backstage/plugin-org@0.4.3 + - @backstage/plugin-pagerduty@0.3.26 + - @backstage/plugin-permission-react@0.3.1 + - @backstage/plugin-rollbar@0.3.27 + - @backstage/plugin-scaffolder@0.12.3 + - @backstage/plugin-search@0.7.0 + - @backstage/plugin-sentry@0.3.37 + - @backstage/plugin-shortcuts@0.2.0 + - @backstage/plugin-tech-insights@0.1.9 + - @backstage/plugin-tech-radar@0.5.6 + - @backstage/plugin-techdocs@0.13.4 + - @backstage/plugin-todo@0.2.1 + - @backstage/plugin-user-settings@0.3.20 + - @backstage/app-defaults@0.1.8 + - @backstage/catalog-model@0.10.0 + - @backstage/search-common@0.2.3 + - @backstage/theme@0.2.15 + - @backstage/plugin-catalog-common@0.1.3 + - @backstage/plugin-catalog-graph@0.2.11 + ## 0.2.64 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index bdade7974a..25dcbbd945 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,65 +1,68 @@ { "name": "example-app", - "version": "0.2.64", + "version": "0.2.66", "private": true, + "backstage": { + "role": "frontend" + }, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^0.1.7", - "@backstage/catalog-model": "^0.9.10", - "@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.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.8", - "@backstage/theme": "^0.2.14", + "@backstage/app-defaults": "^0.1.9", + "@backstage/catalog-model": "^0.11.0", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/integration-react": "^0.1.23", + "@backstage/plugin-airbrake": "^0.3.0", + "@backstage/plugin-api-docs": "^0.8.0", + "@backstage/plugin-azure-devops": "^0.1.16", + "@backstage/plugin-apache-airflow": "^0.1.8", + "@backstage/plugin-badges": "^0.2.24", + "@backstage/plugin-catalog": "^0.9.0", + "@backstage/plugin-catalog-common": "^0.1.4", + "@backstage/plugin-catalog-graph": "^0.2.12", + "@backstage/plugin-catalog-import": "^0.8.3", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-circleci": "^0.3.0", + "@backstage/plugin-cloudbuild": "^0.3.0", + "@backstage/plugin-code-coverage": "^0.1.27", + "@backstage/plugin-cost-insights": "^0.11.22", + "@backstage/plugin-explore": "^0.3.31", + "@backstage/plugin-gcp-projects": "^0.3.19", + "@backstage/plugin-github-actions": "^0.5.0", + "@backstage/plugin-gocd": "^0.1.6", + "@backstage/plugin-graphiql": "^0.2.32", + "@backstage/plugin-home": "^0.4.16", + "@backstage/plugin-jenkins": "^0.6.0", + "@backstage/plugin-kafka": "^0.3.0", + "@backstage/plugin-kubernetes": "^0.6.0", + "@backstage/plugin-lighthouse": "^0.3.0", + "@backstage/plugin-newrelic": "^0.3.18", + "@backstage/plugin-newrelic-dashboard": "^0.1.8", + "@backstage/plugin-org": "^0.5.0", + "@backstage/plugin-pagerduty": "0.3.27", + "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/plugin-rollbar": "^0.4.0", + "@backstage/plugin-scaffolder": "^0.13.0", + "@backstage/plugin-search": "^0.7.1", + "@backstage/plugin-sentry": "^0.3.38", + "@backstage/plugin-shortcuts": "^0.2.1", + "@backstage/plugin-tech-radar": "^0.5.7", + "@backstage/plugin-techdocs": "^0.14.0", + "@backstage/plugin-todo": "^0.2.2", + "@backstage/plugin-user-settings": "^0.3.21", + "@backstage/search-common": "^0.2.4", + "@backstage/plugin-tech-insights": "^0.1.10", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", - "@roadiehq/backstage-plugin-github-insights": "^1.4.2", - "@roadiehq/backstage-plugin-github-pull-requests": "^1.3.2", - "@roadiehq/backstage-plugin-travis-ci": "^1.3.2", - "@roadiehq/backstage-plugin-buildkite": "^1.3.4", + "@roadiehq/backstage-plugin-github-insights": "^1.5.0", + "@roadiehq/backstage-plugin-github-pull-requests": "^1.4.0", + "@roadiehq/backstage-plugin-travis-ci": "^1.3.6", + "@roadiehq/backstage-plugin-buildkite": "^1.3.8", "history": "^5.0.0", "prop-types": "^15.7.2", "react": "^17.0.2", @@ -71,8 +74,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/plugin-permission-react": "^0.3.0", - "@backstage/test-utils": "^0.2.4", + "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/test-utils": "^0.2.6", "@rjsf/core": "^3.2.1", "@testing-library/cypress": "^8.0.2", "@testing-library/jest-dom": "^5.10.1", @@ -84,18 +87,18 @@ "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", "cross-env": "^7.0.0", - "cypress": "^7.3.0", + "cypress": "^9.5.0", "eslint-plugin-cypress": "^2.10.3", "start-server-and-test": "^1.10.11" }, "scripts": { - "start": "backstage-cli app:serve", - "build": "backstage-cli app:build", - "clean": "backstage-cli clean", - "test": "backstage-cli test", + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "test": "backstage-cli package 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", + "lint": "backstage-cli package lint", "cy:dev": "cypress open", "cy:run": "cypress run" }, diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 345915189a..374d637b2e 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -65,7 +65,6 @@ import { import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; import { - DefaultTechDocsHome, TechDocsIndexPage, techdocsPlugin, TechDocsReaderPage, @@ -169,9 +168,7 @@ const routes = ( /> } /> - }> - - + } /> } diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 57f1fa536f..2441ace27b 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -34,10 +34,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 { Shortcuts } from '@backstage/plugin-shortcuts'; import { Sidebar, @@ -89,9 +86,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( } to="/search"> - - - + }> diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index b68b9e9895..68ce5fb497 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -16,15 +16,14 @@ import { EntityLayout } from '@backstage/plugin-catalog'; import { - DefaultStarredEntitiesApi, EntityProvider, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { githubActionsApiRef } from '@backstage/plugin-github-actions'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { MockPermissionApi, - MockStorageApi, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -59,12 +58,7 @@ describe('EntityPage Test', () => { diff --git a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx index 1c96e9325c..61407e2918 100644 --- a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx +++ b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx @@ -13,14 +13,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import React from 'react'; import type { FieldValidation } from '@rjsf/core'; import { createScaffolderFieldExtension, - TextValuePicker, + FieldExtensionComponentProps, scaffolderPlugin, } from '@backstage/plugin-scaffolder'; +import { TextField } from '@material-ui/core'; + +const TextValuePicker = (props: FieldExtensionComponentProps) => { + const { + onChange, + required, + schema: { title, description }, + rawErrors, + formData, + uiSchema: { 'ui:autofocus': autoFocus }, + idSchema, + placeholder, + } = props; + + return ( + onChange(value)} + margin="normal" + error={rawErrors?.length > 0 && !formData} + inputProps={{ autoFocus }} + /> + ); +}; + export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ name: 'LowerCaseValuePicker', diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index d4e60244d7..65da72d91c 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -24,7 +24,7 @@ import { SidebarPinStateContext, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; -import { CatalogResultListItem } from '@backstage/plugin-catalog'; +import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; import { catalogApiRef, CATALOG_FILTER_EXISTS, @@ -38,7 +38,7 @@ import { SearchType, useSearch, } from '@backstage/plugin-search'; -import { DocsResultListItem } from '@backstage/plugin-techdocs'; +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; import React, { useContext } from 'react'; @@ -136,14 +136,14 @@ const SearchPage = () => { switch (type) { case 'software-catalog': return ( - ); case 'techdocs': return ( - diff --git a/packages/app/src/components/techdocs/TechDocsPage.tsx b/packages/app/src/components/techdocs/TechDocsPage.tsx index be11e30e61..4f49cf38e8 100644 --- a/packages/app/src/components/techdocs/TechDocsPage.tsx +++ b/packages/app/src/components/techdocs/TechDocsPage.tsx @@ -16,18 +16,18 @@ import { Content } from '@backstage/core-components'; import { - TechDocsPageHeader, - TechDocsPage, + TechDocsReaderPageHeader, + TechDocsReaderPage, Reader, } from '@backstage/plugin-techdocs'; import React from 'react'; const DefaultTechDocsPage = () => { return ( - + {({ techdocsMetadataValue, entityMetadataValue, entityRef, onReady }) => ( <> - { )} - + ); }; diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 49204d2b5e..5f8c0faf47 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -19,27 +19,12 @@ import { gitlabAuthApiRef, oktaAuthApiRef, githubAuthApiRef, - samlAuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, - oauth2ApiRef, - oidcAuthApiRef, bitbucketAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ - { - id: 'oidc-auth-provider', - title: 'Oidc', - message: 'Sign In using OpenId Connect', - apiRef: oidcAuthApiRef, - }, - { - id: 'oauth2-auth-provider', - title: 'OAuth 2.0', - message: 'Sign In using OAuth 2.0', - apiRef: oauth2ApiRef, - }, { id: 'google-auth-provider', title: 'Google', @@ -70,12 +55,6 @@ export const providers = [ message: 'Sign In using Okta', apiRef: oktaAuthApiRef, }, - { - id: 'saml-auth-provider', - title: 'SAML', - message: 'Sign In using SAML', - apiRef: samlAuthApiRef, - }, { id: 'onelogin-auth-provider', title: 'OneLogin', diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 2e80c62c92..ce5c2f0894 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/backend-common +## 0.11.0 + +### Minor Changes + +- 7d12e4cf32: feat(backend-common): add Redis backed cache store + +### Patch Changes + +- b2f8bb99d3: Make backend.auth.keys optional in config schema. Previously backend.auth was optional but keys was not, which meant that if another plugin introduced additional properties under backend.auth, it would implicitly make backend.auth.keys mandatory. +- d64b8d3678: chore(deps): bump `minimatch` from 3.0.4 to 5.0.0 +- Updated dependencies + - @backstage/config-loader@0.9.6 + - @backstage/integration@0.7.5 + +## 0.10.9 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/cli-common@0.1.8 + - @backstage/config@0.1.15 + - @backstage/config-loader@0.9.5 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + - @backstage/types@0.1.3 + +## 0.10.8 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 0107c9aa08: chore(deps): bump `helmet` from 4.4.1 to 5.0.2 +- b590e9b58d: Updated `isDatabaseConflictError` to handle modern sqlite conflict errors +- Updated dependencies + - @backstage/config-loader@0.9.4 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/cli-common@0.1.7 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.10.7 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 9b963f3fc9..4ed9bb1514 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -234,6 +234,21 @@ export type ErrorHandlerOptions = { logClientErrors?: boolean; }; +// @public +export class FetchUrlReader implements UrlReader { + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(): Promise; + // (undocumented) + readUrl(url: string, options?: ReadUrlOptions): Promise; + // (undocumented) + search(): Promise; + // (undocumented) + toString(): string; +} + // @public export type FromReadableArrayOptions = Array<{ data: Readable; diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 16ca1698f6..0c3e3132c0 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -23,7 +23,7 @@ export interface Config { /** Backend configuration for when request authentication is enabled */ auth?: { /** Keys shared by all backends for signing and validating backend tokens. */ - keys: { + keys?: { /** * Secret for generating tokens. Should be a base64 string, recommended * length is 24 bytes. @@ -134,6 +134,14 @@ export interface Config { | { store: 'memory'; } + | { + store: 'redis'; + /** + * A redis connection string in the form `redis://user:pass@host:port`. + * @secret + */ + connection: string; + } | { store: 'memcache'; /** diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 13a34a1d81..fd5ea443ab 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.10.7", + "version": "0.11.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -11,6 +11,9 @@ "types": "dist/index.d.ts", "alphaTypes": "dist/index.alpha.d.ts" }, + "backstage": { + "role": "node-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,20 +25,21 @@ ], "license": "Apache-2.0", "scripts": { - "build": "backstage-cli build --experimental-type-build --outputs cjs,types", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/cli-common": "^0.1.6", - "@backstage/config": "^0.1.13", - "@backstage/config-loader": "^0.9.3", - "@backstage/errors": "^0.2.0", - "@backstage/integration": "^0.7.2", - "@backstage/types": "^0.1.1", + "@backstage/cli-common": "^0.1.8", + "@backstage/config": "^0.1.15", + "@backstage/config-loader": "^0.9.6", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/types": "^0.1.3", "@google-cloud/storage": "^5.8.0", "@manypkg/get-packages": "^1.1.3", "@octokit/rest": "^18.5.3", @@ -53,16 +57,17 @@ "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", "git-url-parse": "^11.6.0", - "helmet": "^4.0.0", + "helmet": "^5.0.2", "isomorphic-git": "^1.8.0", "jose": "^1.27.1", "keyv": "^4.0.3", "keyv-memcache": "^1.2.5", + "@keyv/redis": "^2.2.3", "knex": "^1.0.2", "lodash": "^4.17.21", "logform": "^2.3.2", "luxon": "^2.0.2", - "minimatch": "^3.0.4", + "minimatch": "^5.0.0", "minimist": "^1.2.5", "morgan": "^1.10.0", "node-abort-controller": "^3.0.1", @@ -84,8 +89,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^2.0.0", diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index ce5b6f5999..c6cad2e49b 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -18,6 +18,8 @@ import { ConfigReader } from '@backstage/config'; import Keyv from 'keyv'; /* @ts-expect-error */ import KeyvMemcache from 'keyv-memcache'; +/* @ts-expect-error */ +import KeyvRedis from '@keyv/redis'; import { DefaultCacheClient } from './CacheClient'; import { CacheManager } from './CacheManager'; import { NoStore } from './NoStore'; @@ -26,6 +28,8 @@ jest.createMockFromModule('keyv'); jest.mock('keyv'); jest.createMockFromModule('keyv-memcache'); jest.mock('keyv-memcache'); +jest.createMockFromModule('@keyv/redis'); +jest.mock('@keyv/redis'); jest.mock('./CacheClient', () => { return { DefaultCacheClient: jest.fn(), @@ -190,6 +194,32 @@ describe('CacheManager', () => { }); }); + it('returns a Redis client when configured', () => { + const redisConnection = 'redis://127.0.0.1:6379'; + const manager = CacheManager.fromConfig( + new ConfigReader({ + backend: { + cache: { + store: 'redis', + connection: redisConnection, + }, + }, + }), + ); + const expectedTtl = 3600; + manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); + + const cache = Keyv as unknown as jest.Mock; + const mockCacheCalls = cache.mock.calls.splice(-1); + expect(mockCacheCalls[0][0]).toMatchObject({ + ttl: expectedTtl, + }); + expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis); + const redis = KeyvRedis as jest.Mock; + const mockRedisCalls = redis.mock.calls.splice(-1); + expect(mockRedisCalls[0][0]).toEqual(redisConnection); + }); + describe('connection errors', () => { it('uses provided logger', () => { // Set up and inject mock logger. diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 66e9849e77..d8652e42d8 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -18,6 +18,8 @@ import { Config } from '@backstage/config'; import Keyv from 'keyv'; // @ts-expect-error import KeyvMemcache from 'keyv-memcache'; +// @ts-expect-error +import KeyvRedis from '@keyv/redis'; import { Logger } from 'winston'; import { getRootLogger } from '../logging'; import { DefaultCacheClient, CacheClient } from './CacheClient'; @@ -37,6 +39,7 @@ export class CacheManager { * that return Keyv instances appropriate to the store. */ private readonly storeFactories = { + redis: this.getRedisClient, memcache: this.getMemcacheClient, memory: this.getMemoryClient, none: this.getNoneClient, @@ -123,6 +126,17 @@ export class CacheManager { return this.storeFactories[this.store].call(this, pluginId, ttl); } + private getRedisClient( + pluginId: string, + defaultTtl: number | undefined, + ): Keyv { + return new Keyv({ + namespace: pluginId, + ttl: defaultTtl, + store: new KeyvRedis(this.connection), + }); + } + private getMemcacheClient( pluginId: string, defaultTtl: number | undefined, diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index ea8e4626be..371e11b364 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -270,22 +270,6 @@ describe('BitbucketUrlReader', () => { expect(response.etag).toBe('12ab34cd56ef'); }); - - it('should throw error when apiBaseUrl is missing', () => { - expect(() => { - /* eslint-disable no-new */ - new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( - new ConfigReader({ - host: 'bitbucket.mycompany.net', - }), - ), - ), - { treeResponseFactory }, - ); - }).toThrowError('must configure an explicit apiBaseUrl'); - }); }); describe('search hosted', () => { diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 2006637545..e22137ea69 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -62,14 +62,9 @@ export class BitbucketUrlReader implements UrlReader { private readonly integration: BitbucketIntegration, private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { - const { host, apiBaseUrl, token, username, appPassword } = - integration.config; + const { host, token, username, appPassword } = integration.config; - if (!apiBaseUrl) { - throw new Error( - `Bitbucket integration for '${host}' must configure an explicit apiBaseUrl`, - ); - } else if (!token && username && !appPassword) { + if (!token && username && !appPassword) { throw new Error( `Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`, ); diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index 2c3394e4fc..9c29bc93fe 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -19,6 +19,7 @@ export { BitbucketUrlReader } from './BitbucketUrlReader'; export { GithubUrlReader } from './GithubUrlReader'; export { GitlabUrlReader } from './GitlabUrlReader'; export { AwsS3UrlReader } from './AwsS3UrlReader'; +export { FetchUrlReader } from './FetchUrlReader'; export type { FromReadableArrayOptions, ReaderFactory, diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index d155287b34..105704159b 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -19,7 +19,7 @@ import compression from 'compression'; import cors from 'cors'; import express, { Router, ErrorRequestHandler } from 'express'; import helmet from 'helmet'; -import { ContentSecurityPolicyOptions } from 'helmet/dist/middlewares/content-security-policy'; +import { ContentSecurityPolicyOptions } from 'helmet/dist/types/middlewares/content-security-policy'; import * as http from 'http'; import stoppable from 'stoppable'; import { Logger } from 'winston'; @@ -215,8 +215,17 @@ export class ServiceBuilderImpl implements ServiceBuilder { httpsSettings: this.httpsSettings, helmetOptions: { contentSecurityPolicy: { + useDefaults: false, directives: applyCspDirectives(this.cspOptions), }, + // These are all disabled in order to maintain backwards compatibility + // when bumping helmet v5. We can't enable these by default because + // there is no way for users to configure them. + // TODO(Rugvip): We should give control of this setup to consumers + crossOriginEmbedderPolicy: false, + crossOriginOpenerPolicy: false, + crossOriginResourcePolicy: false, + originAgentCluster: false, }, }; } @@ -232,6 +241,11 @@ export function applyCspDirectives( // It should be replaced by any other solution that doesn't require unsafe-eval. result['script-src'] = ["'self'", "'unsafe-eval'"]; + // TODO(Rugvip): This is removed so that we maintained backwards compatibility + // when bumping to helmet v5, we could remove this as well as + // skip setting `useDefaults: false` in the future. + delete result['form-action']; + if (directives) { for (const [key, value] of Object.entries(directives)) { if (value === false) { diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index d611ee8ac1..546d4a8187 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/backend-tasks +## 0.1.9 + +### Patch Changes + +- dc97845422: Only output janitor logs when actually timing out tasks +- Updated dependencies + - @backstage/backend-common@0.11.0 + +## 0.1.8 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/types@0.1.3 + +## 0.1.7 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.1.6 ### Patch Changes diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 364f1d1380..215fafe970 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.6", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "node-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,18 +24,19 @@ ], "license": "Apache-2.0", "scripts": { - "build": "backstage-cli build --outputs cjs,types", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", - "@backstage/types": "^0.1.1", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/types": "^0.1.3", "@types/luxon": "^2.0.4", "knex": "^1.0.2", "lodash": "^4.17.21", @@ -43,8 +47,8 @@ "zod": "^3.9.5" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.17", - "@backstage/cli": "^0.13.2", + "@backstage/backend-test-utils": "^0.1.19", + "@backstage/cli": "^0.14.1", "jest": "^26.0.1", "wait-for-expect": "^3.0.2" }, diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts index 8b90afff42..c77fe4e925 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts @@ -62,19 +62,21 @@ export class PluginTaskSchedulerJanitor { const dbNull = this.knex.raw('null'); - const tasksQuery = this.knex(DB_TASKS_TABLE) + const tasks = await this.knex(DB_TASKS_TABLE) .where('current_run_expires_at', '<', this.knex.fn.now()) .update({ current_run_ticket: dbNull, current_run_started_at: dbNull, current_run_expires_at: dbNull, - }); + }) + .returning(['id']); - if (this.knex.client.config.client === 'sqlite3') { - const tasks = await tasksQuery; - this.logger.warn(`${tasks} tasks timed out and were lost`); + // sqlite ignores "returning", returns number of rows changed instead + if (typeof tasks === 'number') { + if (tasks > 0) { + this.logger.warn(`${tasks} tasks timed out and were lost`); + } } else { - const tasks = await tasksQuery.returning(['id']); for (const { id } of tasks) { this.logger.warn(`Task timed out and was lost: ${id}`); } diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 55fb3c94cd..03f564074b 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/backend-test-utils +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/cli@0.14.1 + +## 0.1.18 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/cli@0.14.0 + - @backstage/backend-common@0.10.8 + - @backstage/config@0.1.14 + ## 0.1.17 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 8a9767b292..2cc3b8a07b 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.17", + "version": "0.1.19", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "node-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,17 +25,18 @@ ], "license": "Apache-2.0", "scripts": { - "build": "backstage-cli build --outputs cjs,types", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/cli": "^0.13.2", - "@backstage/config": "^0.1.13", + "@backstage/backend-common": "^0.11.0", + "@backstage/cli": "^0.14.1", + "@backstage/config": "^0.1.15", "@vscode/sqlite3": "^5.0.7", "knex": "^1.0.2", "mysql2": "^2.2.5", @@ -41,7 +45,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index d68ec80469..227b8ee231 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,120 @@ # example-backend +## 0.2.66 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-catalog-backend@0.22.0 + - @backstage/plugin-scaffolder-backend@0.17.0 + - @backstage/plugin-graphql-backend@0.1.17 + - @backstage/plugin-auth-backend@0.11.0 + - @backstage/plugin-kubernetes-backend@0.4.10 + - @backstage/plugin-code-coverage-backend@0.1.25 + - @backstage/plugin-jenkins-backend@0.1.16 + - @backstage/plugin-tech-insights-backend@0.2.7 + - @backstage/plugin-todo-backend@0.1.24 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/plugin-badges-backend@0.1.21 + - @backstage/backend-tasks@0.1.9 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.2 + - @backstage/plugin-techdocs-backend@0.14.0 + - @backstage/plugin-permission-node@0.5.2 + - @backstage/integration@0.7.5 + - example-app@0.2.66 + - @backstage/plugin-app-backend@0.3.27 + - @backstage/plugin-auth-node@0.1.3 + - @backstage/plugin-azure-devops-backend@0.3.6 + - @backstage/plugin-kafka-backend@0.2.20 + - @backstage/plugin-permission-backend@0.5.2 + - @backstage/plugin-proxy-backend@0.2.21 + - @backstage/plugin-rollbar-backend@0.1.24 + - @backstage/plugin-search-backend@0.4.5 + - @backstage/plugin-search-backend-module-pg@0.2.9 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.11 + - @backstage/plugin-tech-insights-node@0.2.5 + +## 0.2.66 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/backend-tasks@0.1.8 + - @backstage/catalog-client@0.7.1 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/integration@0.7.4 + - @backstage/plugin-app-backend@0.3.26 + - @backstage/plugin-auth-backend@0.10.2 + - @backstage/plugin-auth-node@0.1.2 + - @backstage/plugin-azure-devops-backend@0.3.5 + - @backstage/plugin-badges-backend@0.1.20 + - @backstage/plugin-catalog-backend@0.21.5 + - @backstage/plugin-code-coverage-backend@0.1.24 + - @backstage/plugin-graphql-backend@0.1.16 + - @backstage/plugin-jenkins-backend@0.1.15 + - @backstage/plugin-kafka-backend@0.2.19 + - @backstage/plugin-kubernetes-backend@0.4.9 + - @backstage/plugin-permission-backend@0.5.1 + - @backstage/plugin-permission-common@0.5.1 + - @backstage/plugin-permission-node@0.5.1 + - @backstage/plugin-proxy-backend@0.2.20 + - @backstage/plugin-rollbar-backend@0.1.23 + - @backstage/plugin-scaffolder-backend@0.16.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.1 + - @backstage/plugin-search-backend@0.4.4 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.10 + - @backstage/plugin-search-backend-module-pg@0.2.8 + - @backstage/plugin-search-backend-node@0.4.7 + - @backstage/plugin-tech-insights-backend@0.2.6 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.10 + - @backstage/plugin-tech-insights-node@0.2.4 + - @backstage/plugin-techdocs-backend@0.13.5 + - @backstage/plugin-todo-backend@0.1.23 + +## 0.2.65 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@0.13.4 + - @backstage/plugin-catalog-backend@0.21.4 + - @backstage/backend-common@0.10.8 + - @backstage/catalog-client@0.7.0 + - @backstage/integration@0.7.3 + - @backstage/plugin-auth-backend@0.10.1 + - @backstage/plugin-auth-node@0.1.1 + - @backstage/plugin-permission-backend@0.5.0 + - @backstage/plugin-permission-common@0.5.0 + - @backstage/plugin-rollbar-backend@0.1.22 + - @backstage/plugin-scaffolder-backend@0.16.0 + - @backstage/backend-tasks@0.1.7 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/plugin-app-backend@0.3.25 + - @backstage/plugin-azure-devops-backend@0.3.4 + - @backstage/plugin-badges-backend@0.1.19 + - @backstage/plugin-code-coverage-backend@0.1.23 + - @backstage/plugin-graphql-backend@0.1.15 + - @backstage/plugin-jenkins-backend@0.1.14 + - @backstage/plugin-kafka-backend@0.2.18 + - @backstage/plugin-kubernetes-backend@0.4.8 + - @backstage/plugin-permission-node@0.5.0 + - @backstage/plugin-proxy-backend@0.2.19 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.0 + - @backstage/plugin-search-backend@0.4.3 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.9 + - @backstage/plugin-search-backend-module-pg@0.2.7 + - @backstage/plugin-search-backend-node@0.4.6 + - @backstage/plugin-tech-insights-backend@0.2.5 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.9 + - @backstage/plugin-tech-insights-node@0.2.3 + - @backstage/plugin-todo-backend@0.1.22 + - example-app@0.2.65 + ## 0.2.64 ### Patch Changes diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile index 31231a3a4a..dd35d4ddbc 100644 --- a/packages/backend/Dockerfile +++ b/packages/backend/Dockerfile @@ -9,7 +9,7 @@ # # Once the commands have been run, you can build the image using `yarn build-image` -FROM node:14-buster-slim +FROM node:16-bullseye-slim WORKDIR /app @@ -19,6 +19,12 @@ WORKDIR /app COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz +# install sqlite3 dependencies +RUN apt-get update && \ + apt-get install -y libsqlite3-dev python3 cmake g++ && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" # Then copy the rest of the backend bundle, along with any other files we might want. diff --git a/packages/backend/package.json b/packages/backend/package.json index 35b234aeda..cc960e3c24 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,10 +1,13 @@ { "name": "example-backend", - "version": "0.2.64", + "version": "0.2.66", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "backstage": { + "role": "backend" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -15,48 +18,48 @@ "backstage" ], "scripts": { - "build": "backstage-cli backend:bundle", + "build": "backstage-cli package build", "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", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@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.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.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.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", + "@backstage/backend-common": "^0.11.0", + "@backstage/backend-tasks": "^0.1.9", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/integration": "^0.7.5", + "@backstage/plugin-app-backend": "^0.3.27", + "@backstage/plugin-auth-backend": "^0.11.0", + "@backstage/plugin-auth-node": "^0.1.3", + "@backstage/plugin-azure-devops-backend": "^0.3.6", + "@backstage/plugin-badges-backend": "^0.1.21", + "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-code-coverage-backend": "^0.1.25", + "@backstage/plugin-graphql-backend": "^0.1.17", + "@backstage/plugin-jenkins-backend": "^0.1.16", + "@backstage/plugin-kubernetes-backend": "^0.4.10", + "@backstage/plugin-kafka-backend": "^0.2.20", + "@backstage/plugin-permission-backend": "^0.5.2", + "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-permission-node": "^0.5.2", + "@backstage/plugin-proxy-backend": "^0.2.21", + "@backstage/plugin-rollbar-backend": "^0.1.24", + "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.3.2", + "@backstage/plugin-search-backend": "^0.4.5", + "@backstage/plugin-search-backend-node": "^0.4.7", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.10", + "@backstage/plugin-search-backend-module-pg": "^0.2.9", + "@backstage/plugin-techdocs-backend": "^0.14.0", + "@backstage/plugin-tech-insights-backend": "^0.2.7", + "@backstage/plugin-tech-insights-node": "^0.2.5", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.11", + "@backstage/plugin-todo-backend": "^0.1.24", "@gitbeaker/node": "^35.1.0", "@octokit/rest": "^18.5.3", "@vscode/sqlite3": "^5.0.7", @@ -73,7 +76,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@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 276d16ff97..557cea9a26 100644 --- a/packages/backend/src/plugins/permission.ts +++ b/packages/backend/src/plugins/permission.ts @@ -35,8 +35,9 @@ 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(), diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 4b5363e73d..a90b47cd81 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -18,7 +18,7 @@ import { useHotCleanup, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; import { createRouter } from '@backstage/plugin-search-backend'; import { ElasticSearchSearchEngine } from '@backstage/plugin-search-backend-module-elasticsearch'; import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg'; @@ -27,7 +27,7 @@ import { LunrSearchEngine, SearchEngine, } from '@backstage/plugin-search-backend-node'; -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; import { Logger } from 'winston'; import { PluginEnvironment } from '../types'; @@ -70,7 +70,7 @@ export default async function createPlugin({ // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultCatalogCollator.fromConfig(config, { + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery, tokenManager, }), @@ -78,7 +78,7 @@ export default async function createPlugin({ indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, logger, tokenManager, diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 664e7a49ed..93f248ff0e 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/catalog-client +## 0.7.2 + +### Patch Changes + +- ed09ad8093: Updated usage of the `LocationSpec` type from `@backstage/catalog-model`, which is deprecated. +- 46dee04eba: Deprecated `AddLocationRequest.presence`, as it is already being ignored. +- 6e1cbc12a6: Added `CatalogApi.getEntityFacets`. Marking this as a breaking change since it + is a non-optional addition to the API and depends on the backend being in place. + If you are mocking this interface in your tests, you will need to add an extra + `getEntityFacets: jest.fn()` or similar to that interface. +- Updated dependencies + - @backstage/catalog-model@0.11.0 + +## 0.7.1 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/catalog-model@0.10.1 + - @backstage/errors@0.2.2 + +## 0.7.0 + +### Minor Changes + +- 8eda0e7a9c: **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. + +- deaf6065db: Removed `CatalogApi.geLocationByEntity` and `CatalogApi.getOriginLocationByEntity`, and replaced them with `CatalogApi.getLocationByRef`. + + If you were using one of the two old methods, you can update your code as follows: + + ```diff + -const originLocation = catalogApi.getOriginLocationByEntity(entity); + +const originLocation = catalogApi.getLocationByRef(entity.metadata.annotations[ANNOTATION_ORIGIN_LOCATION]!); + -const location = catalogApi.getLocationByEntity(entity); + +const location = catalogApi.getLocationByRef(entity.metadata.annotations[ANNOTATION_LOCATION]!); + ``` + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 216725b434: Updated to use new names for `parseLocationRef` and `stringifyLocationRef` +- 244d24ebc4: Export the `Location` type that was previously exported by the `@backstage/catalog-model` package. +- 538ca90790: 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` + +- 27eccab216: Replaces use of deprecated catalog-model constants. +- Updated dependencies + - @backstage/errors@0.2.1 + - @backstage/catalog-model@0.10.0 + ## 0.6.0 ### Minor Changes diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index a489b32179..c957859fec 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -5,7 +5,6 @@ ```ts import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; -import { Location as Location_2 } from '@backstage/catalog-model'; // @public export type AddLocationRequest = { @@ -32,27 +31,27 @@ 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, ): Promise; - getLocationByEntity( - entity: Entity, + getEntityFacets( + request: GetEntityFacetsRequest, options?: CatalogRequestOptions, - ): Promise; + ): Promise; getLocationById( id: string, options?: CatalogRequestOptions, ): Promise; - getOriginLocationByEntity( - entity: Entity, + getLocationByRef( + locationRef: string, options?: CatalogRequestOptions, ): Promise; refreshEntity( @@ -84,17 +83,22 @@ export class CatalogClient implements CatalogApi { 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, ): Promise; + getEntityFacets( + request: GetEntityFacetsRequest, + options?: CatalogRequestOptions, + ): Promise; + // @deprecated (undocumented) getLocationByEntity( entity: Entity, options?: CatalogRequestOptions, @@ -103,6 +107,11 @@ export class CatalogClient implements CatalogApi { id: string, options?: CatalogRequestOptions, ): Promise; + getLocationByRef( + locationRef: string, + options?: CatalogRequestOptions, + ): Promise; + // @deprecated (undocumented) getOriginLocationByEntity( entity: Entity, options?: CatalogRequestOptions, @@ -121,43 +130,89 @@ 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 = { - rootEntityRef: string; - items: { - entity: Entity; - parentEntityRefs: string[]; - }[]; -}; - -// @public -export type CatalogListResponse = { - items: T[]; -}; - -// @public -export type CatalogRequestOptions = { +export interface CatalogRequestOptions { + // (undocumented) token?: string; -}; +} // @public export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = 'backstage.io/catalog-processing'; + +// @public +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 +export interface GetEntityFacetsRequest { + facets: string[]; + filter?: + | Record[] + | Record + | undefined; +} + +// @public +export interface GetEntityFacetsResponse { + facets: Record< + string, + Array<{ + value: string; + count: number; + }> + >; +} + +// @public +type Location_2 = { + id: string; + type: string; + target: string; + presence?: 'optional' | 'required'; +}; +export { Location_2 as Location }; ``` diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 3a45a74680..ffcfbc16b7 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.6.0", + "version": "0.7.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -12,6 +12,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,20 +25,20 @@ "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/errors": "^0.2.0", + "@backstage/catalog-model": "^0.11.0", + "@backstage/errors": "^0.2.2", "cross-fetch": "^3.1.5" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@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 009bc9a6cb..bb3c9604b3 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,20 +30,24 @@ import { AddLocationRequest, AddLocationResponse, CatalogApi, - CatalogEntitiesRequest, - CatalogListResponse, + GetEntitiesRequest, + GetEntitiesResponse, CatalogRequestOptions, - CatalogEntityAncestorsRequest, - CatalogEntityAncestorsResponse, + GetEntityAncestorsRequest, + GetEntityAncestorsResponse, + Location, + GetEntityFacetsRequest, + GetEntityFacetsResponse, } 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; @@ -58,19 +61,12 @@ export class CatalogClient implements CatalogApi { } /** - * 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', @@ -82,14 +78,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, @@ -103,28 +92,20 @@ 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[] = []; // filter param can occur multiple times, for example // /api/catalog/entities?filter=metadata.name=wayback-search,kind=component&filter=metadata.name=www-artist,kind=component' // the "outer array" defined by `filter` occurrences corresponds to "anyOf" filters // the "inner array" defined within a `filter` param corresponds to "allOf" filters - for (const filterItem of filterItems) { + for (const filterItem of [filter].flat()) { const filterParts: string[] = []; for (const [key, value] of Object.entries(filterItem)) { for (const v of [value].flat()) { @@ -190,14 +171,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, @@ -214,12 +188,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( @@ -240,14 +209,48 @@ 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.getEntityFacets} + */ + async getEntityFacets( + request: GetEntityFacetsRequest, + options?: CatalogRequestOptions, + ): Promise { + const { filter = [], facets } = request; + const params: string[] = []; + + // filter param can occur multiple times, for example + // /api/catalog/entities?filter=metadata.name=wayback-search,kind=component&filter=metadata.name=www-artist,kind=component' + // the "outer array" defined by `filter` occurrences corresponds to "anyOf" filters + // the "inner array" defined within a `filter` param corresponds to "allOf" filters + for (const filterItem of [filter].flat()) { + const filterParts: string[] = []; + for (const [key, value] of Object.entries(filterItem)) { + for (const v of [value].flat()) { + if (v === CATALOG_FILTER_EXISTS) { + filterParts.push(encodeURIComponent(key)); + } else if (typeof v === 'string') { + filterParts.push( + `${encodeURIComponent(key)}=${encodeURIComponent(v)}`, + ); + } + } + } + + if (filterParts.length) { + params.push(`filter=${filterParts.join(',')}`); + } + } + + for (const facet of facets) { + params.push(`facet=${encodeURIComponent(facet)}`); + } + + const query = params.length ? `?${params.join('&')}` : ''; + return await this.requestOptional('GET', `/entity-facets${query}`, options); + } + + /** + * {@inheritdoc CatalogApi.addLocation} */ async addLocation( { type = 'url', target, dryRun, presence }: AddLocationRequest, @@ -285,21 +288,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 + * @deprecated please use getLocationByRef instead */ 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; } @@ -310,24 +306,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 + * @deprecated please use getLocationByRef instead */ 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; } @@ -338,16 +327,28 @@ 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.getLocationByRef} + */ + async getLocationByRef( + locationRef: string, + options?: CatalogRequestOptions, + ): Promise { + const all: { data: Location }[] = await this.requestRequired( + 'GET', + '/locations', + options, + ); + return all + .map(r => r.data) + .find(l => locationRef === stringifyLocationRef(l)); + } + + /** + * {@inheritdoc CatalogApi.removeLocationById} */ async removeLocationById( id: string, @@ -361,12 +362,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 2177c41aa2..4a86ca41f3 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -import { Entity, EntityName, Location } from '@backstage/catalog-model'; +import { Entity, EntityName } 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 */ @@ -27,11 +29,11 @@ export const CATALOG_FILTER_EXISTS = Symbol.for( ); /** - * 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. * @@ -107,175 +109,147 @@ 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 = { +export interface GetEntityAncestorsResponse { rootEntityRef: string; - items: { entity: Entity; parentEntityRefs: string[] }[]; -}; + items: Array<{ + entity: Entity; + parentEntityRefs: string[]; + }>; +} /** - * A response type for the result of a catalog operation in list form. + * The request type for {@link CatalogClient.getEntityFacets}. * * @public */ -export type CatalogListResponse = { - items: T[]; -}; +export interface GetEntityFacetsRequest { + /** + * If given, return only entities that match the given patterns. + * + * @remarks + * + * If multiple filter sets are given as an array, then there is effectively an + * OR between each filter set. + * + * Within one filter set, there is effectively an AND between the various + * keys. + * + * Within one key, if there are more than one value, then there is effectively + * an OR between them. + * + * Example: For an input of + * + * ``` + * [ + * { kind: ['API', 'Component'] }, + * { 'metadata.name': 'a', 'metadata.namespace': 'b' } + * ] + * ``` + * + * This effectively means + * + * ``` + * (kind = EITHER 'API' OR 'Component') + * OR + * (metadata.name = 'a' AND metadata.namespace = 'b' ) + * ``` + * + * Each key is a dot separated path in each object. + * + * As a value you can also pass in the symbol `CATALOG_FILTER_EXISTS` + * (exported from this package), which means that you assert on the existence + * of that key, no matter what its value is. + */ + filter?: + | Record[] + | Record + | undefined; + /** + * Dot separated paths for the facets to extract from each entity. + * + * @remarks + * + * Example: For an input of `['kind', 'metadata.annotations.backstage.io/orphan']`, then the + * response will be shaped like + * + * ``` + * { + * "facets": { + * "kind": [ + * { "key": "Component", "count": 22 }, + * { "key": "API", "count": 13 } + * ], + * "metadata.annotations.backstage.io/orphan": [ + * { "key": "true", "count": 2 } + * ] + * } + * } + * ``` + */ + facets: string[]; +} + +/** + * The response type for {@link CatalogClient.getEntityFacets}. + * + * @public + */ +export interface GetEntityFacetsResponse { + /** + * The computed facets, one entry per facet in the request. + */ + facets: Record>; +} /** * 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; + type: string; + target: string; + /** @deprecated This field is is ignored */ + presence?: 'optional' | 'required'; +}; + +/** + * The request type for {@link CatalogClient.addLocation}. * * @public */ @@ -283,17 +257,140 @@ export type AddLocationRequest = { type?: string; target: string; dryRun?: boolean; + /** @deprecated This field is is ignored */ presence?: 'optional' | 'required'; }; /** - * 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; + + /** + * Gets a summary of field facets of entities in the catalog. + * + * @param request - Request parameters + * @param options - Additional options + */ + getEntityFacets( + request: GetEntityFacetsRequest, + 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 a registered location by its ref. + * + * @param locationRef - A location ref, e.g. "url:https://github.com/..." + * @param options - Additional options + */ + getLocationByRef( + locationRef: string, + 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 cb6166c6d5..39c8962b68 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -14,15 +14,19 @@ * 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, + GetEntityFacetsRequest, + GetEntityFacetsResponse, } from './api'; -export { CATALOG_FILTER_EXISTS } from './api'; +export * from './deprecated'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 9dbf2f116b..cbb2e681bf 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/catalog-model +## 0.11.0 + +### Minor Changes + +- 919cf2f836: **BREAKING**: Added `EntityRelation.targetRef` (a string form entity ref), and + deprecated `EntityRelation.target` (a kind + namespace + name object). This + aligns the type with our goal of using string form entity refs more coherently. + There will be a period of time when both fields need to be present, which may in + particular affect your tests which now have to list both the string form and the + object form side by side. + + ```diff + const mockEntity: Entity = { + kind: 'Component', + relations: [ + { + type: RELATION_MEMBER_OF, + + targetRef: 'component:default/foo', + target: { kind: 'component', namespace: 'default', name: 'foo' } + } + ] + ``` + + The `target` field will be entirely removed from this type in a future release. + +- 7010349c9a: **BREAKING**: Removed `EntityRelationSpec` as it is only used during the catalog during the catalog processing. +- 68f0871b76: **BREAKING**: Simplified the `parseEntityRef` function to _always_ either return + a complete `EntityName`, complete with both kind, namespace and name, or throw + an error if it for some reason did not have enough information to form that + result. This makes its usage and its type declaration vastly simpler. + +### Patch Changes + +- ed09ad8093: Deprecated the `LocationSpec` export, which should now be imported from `@backstage/plugin-catalog-backend` instead. +- b5c02d10a6: Remove all usage of the deprecated `ENTITY_DEFAULT_NAMESPACE` +- 67a7c02d26: **DEPRECATED**: The `EntityRef` type and `parseEntityName` function are now + deprecated, and will soon be removed. This is part of a larger movement toward + fixing the poorly named `EntityName` type which should instead have been named + `EntityRef`. Please remove any usage of these as soon as possible. +- da44c47f43: Deprecated the `entity.metadata.generation` as the field has never been fully functioning. + +## 0.10.1 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/types@0.1.3 + +## 0.10.0 + +### Minor Changes + +- e483dd6c72: **BREAKING**: Remove deprecated validation methods `analyzeLocationSchema`, `locationSchema` and `locationSpecSchema`. + This functionality was primarily used internally by the `catalog-backend`. +- edbc03814a: **BREAKING**: Remove deprecated `serializeEntityRef` which is replaced by `stringifyEntityRef`. +- e72d371296: **BREAKING**: Removed `TemplateEntityV1beta2` from the model and moved it to + `@backstage/plugin-scaffolder-common` where `TemplateEntityV1beta3` already + lived. It has also been marked as deprecated in the process - please consider + [migrating to `v1beta3` templates](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3). + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 216725b434: Deprecated `parseLocationReference` and `stringifyLocationReference`, + introducing `parseLocationRef` and `stringifyLocationRef` in their place. +- 244d24ebc4: Deprecates the `Location` type export from this package. Imports of the `Location` type should now be done from the `@backstage/catalog-client` package instead. +- 7aeb491394: Deprecated `ENTITY_DEFAULT_NAMESPACE` constant in favour of `DEFAULT_NAMESPACE`. +- 0d03e42a89: Deprecated `compareEntityToRef` due to low usage and provided value. +- e8c6f9d282: Deprecated the `EntityRefContext` type which had limited use. +- 7f21538c9e: Deprecated `JSONSchema` export type. +- 20f3244102: Deprecates the following exports which all have very limited and internal use. `ENTITY_META_GENERATED_FIELDS`, `entityHasChanges`, `generateEntityEtag`, `generateEntityUid`, `generateUpdatedEntity`, `ENTITY_META_GENERATED_FIELDS`. +- 27eccab216: 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` +- Updated dependencies + - @backstage/errors@0.2.1 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.9.10 ### Patch Changes diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 9cdb29a15c..4574129d6f 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -4,8 +4,6 @@ ```ts import { JsonObject } from '@backstage/types'; -import { JSONSchema7 } from 'json-schema'; -import { JsonValue } from '@backstage/types'; import { SerializedError } from '@backstage/errors'; // @alpha @@ -13,6 +11,22 @@ export interface AlphaEntity extends Entity { status?: EntityStatus; } +// @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 { // (undocumented) @@ -50,13 +64,6 @@ export class CommonValidatorFunctions { static isValidUrl(value: unknown): boolean; } -// @public -export function compareEntityToRef( - entity: Entity, - ref: EntityRef | EntityName, - context?: EntityRefContext, -): boolean; - // @public interface ComponentEntityV1alpha1 extends Entity { // (undocumented) @@ -81,6 +88,9 @@ export { ComponentEntityV1alpha1 }; // @public export const componentEntityV1alpha1Validator: KindValidator; +// @public +export const DEFAULT_NAMESPACE = 'default'; + // @public export class DefaultNamespaceEntityPolicy implements EntityPolicy { constructor(namespace?: string); @@ -105,9 +115,6 @@ export { DomainEntityV1alpha1 }; // @public export const domainEntityV1alpha1Validator: KindValidator; -// @public -export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; - // @public export type Entity = { apiVersion: string; @@ -117,16 +124,6 @@ export type Entity = { relations?: EntityRelation[]; }; -// @public -export const ENTITY_DEFAULT_NAMESPACE = 'default'; - -// @public -export const ENTITY_META_GENERATED_FIELDS: readonly [ - 'uid', - 'etag', - 'generation', -]; - // @public export type EntityEnvelope = { apiVersion: string; @@ -142,9 +139,6 @@ export function entityEnvelopeSchemaValidator< T extends EntityEnvelope = EntityEnvelope, >(schema?: unknown): (data: unknown) => T; -// @public -export function entityHasChanges(previous: Entity, next: Entity): boolean; - // @public export function entityKindSchemaValidator( schema: unknown, @@ -190,7 +184,7 @@ export type EntityPolicy = { enforce(entity: Entity): Promise; }; -// @public +// @public @deprecated export type EntityRef = | string | { @@ -199,23 +193,11 @@ export type EntityRef = name: string; }; -// @public -export type EntityRefContext = { - defaultKind?: string; - defaultNamespace?: string; -}; - // @public export type EntityRelation = { type: string; target: EntityName; -}; - -// @public -export type EntityRelationSpec = { - source: EntityName; - type: string; - target: EntityName; + targetRef: string; }; // @public @@ -246,15 +228,6 @@ export class FieldFormatEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public -export function generateEntityEtag(): string; - -// @public -export function generateEntityUid(): string; - -// @public -export function generateUpdatedEntity(previous: Entity, next: Entity): Entity; - // @public export function getEntityName(entity: Entity): EntityName; @@ -289,11 +262,6 @@ export { GroupEntityV1alpha1 }; // @public export const groupEntityV1alpha1Validator: KindValidator; -// @public -export type JSONSchema = JSONSchema7 & { - [key in string]?: JsonValue; -}; - // @public export type KindValidator = { check(entity: Entity): Promise; @@ -319,15 +287,6 @@ export class KubernetesValidatorFunctions { static isValidObjectName(value: unknown): boolean; } -// @public -type Location_2 = { - id: string; -} & LocationSpec; -export { Location_2 as Location }; - -// @public -export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; - // @public interface LocationEntityV1alpha1 extends Entity { // (undocumented) @@ -348,7 +307,7 @@ export { LocationEntityV1alpha1 }; // @public export const locationEntityV1alpha1Validator: KindValidator; -// @public +// @public @deprecated export type LocationSpec = { type: string; target: string; @@ -365,51 +324,38 @@ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public -export const ORIGIN_LOCATION_ANNOTATION = - 'backstage.io/managed-by-origin-location'; - -// @public +// @public @deprecated export function parseEntityName( - ref: EntityRef, - context?: EntityRefContext, -): EntityName; - -// @public -export function parseEntityRef( - ref: EntityRef, + ref: + | string + | { + kind?: string; + namespace?: string; + name: string; + }, context?: { - defaultKind: string; - defaultNamespace: string; + defaultKind?: string; + defaultNamespace?: string; }, ): EntityName; // @public export function parseEntityRef( - ref: EntityRef, + ref: + | string + | { + kind?: string; + namespace?: string; + name: string; + }, context?: { - defaultKind: string; + defaultKind?: string; + defaultNamespace?: string; }, -): { - kind: string; - namespace?: string; - name: string; -}; +): EntityName; // @public -export function parseEntityRef( - ref: EntityRef, - context?: { - defaultNamespace: string; - }, -): { - kind?: string; - namespace: string; - name: string; -}; - -// @public -export function parseLocationReference(ref: string): { +export function parseLocationRef(ref: string): { type: string; target: string; }; @@ -483,20 +429,6 @@ export class SchemaValidEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @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 export function stringifyEntityRef( ref: @@ -509,7 +441,7 @@ export function stringifyEntityRef( ): string; // @public -export function stringifyLocationReference(ref: { +export function stringifyLocationRef(ref: { type: string; target: string; }): string; @@ -532,33 +464,6 @@ export { SystemEntityV1alpha1 }; // @public export const systemEntityV1alpha1Validator: KindValidator; -// @public -export interface TemplateEntityV1beta2 extends Entity { - // (undocumented) - apiVersion: 'backstage.io/v1beta2'; - // (undocumented) - kind: 'Template'; - // (undocumented) - spec: { - type: string; - parameters?: JsonObject | JsonObject[]; - steps: Array<{ - id?: string; - name?: string; - action: string; - input?: JsonObject; - if?: string | boolean; - }>; - output?: { - [name: string]: string; - }; - owner?: string; - }; -} - -// @public -export const templateEntityV1beta2Validator: KindValidator; - // @public interface UserEntityV1alpha1 extends Entity { // (undocumented) @@ -593,7 +498,4 @@ export type Validators = { isValidAnnotationValue(value: unknown): boolean; isValidTag(value: unknown): boolean; }; - -// @public -export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; ``` diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 62e577df4a..70ed0fbe4d 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "0.9.10", + "version": "0.11.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -13,6 +13,9 @@ "types": "dist/index.d.ts", "alphaTypes": "dist/index.alpha.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -23,17 +26,17 @@ "backstage" ], "scripts": { - "build": "backstage-cli build --experimental-type-build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", - "@backstage/types": "^0.1.1", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/types": "^0.1.3", "@types/json-schema": "^7.0.5", "ajv": "^7.0.3", "json-schema": "^0.4.0", @@ -41,7 +44,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", + "@backstage/cli": "^0.14.1", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index f56774025c..ef9b4352da 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -116,6 +116,7 @@ export type EntityMeta = JsonObject & { * This field can not be set by the user at creation time, and the server * will reject an attempt to do so. The field will be populated in read * operations. + * @deprecated field is not supported. */ generation?: number; @@ -197,30 +198,15 @@ export type EntityRelation = { /** * The target entity of this relation. + * + * @deprecated use targetRef instead */ target: EntityName; -}; - -/** - * Holds the relation data for entities. - * - * @public - */ -export type EntityRelationSpec = { - /** - * The source entity of this relation. - */ - source: EntityName; /** - * The type of the relation. + * The entity ref of the target of this relation. */ - type: string; - - /** - * The target entity of this relation. - */ - target: EntityName; + targetRef: string; }; /** diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts index 6daf6b78d9..5dc21683bd 100644 --- a/packages/catalog-model/src/entity/constants.ts +++ b/packages/catalog-model/src/entity/constants.ts @@ -19,29 +19,18 @@ * * @public */ -export const ENTITY_DEFAULT_NAMESPACE = 'default'; - -/** - * The keys of EntityMeta that are auto-generated. - * - * @public - */ -export const ENTITY_META_GENERATED_FIELDS = [ - 'uid', - 'etag', - 'generation', -] as const; +export const DEFAULT_NAMESPACE = 'default'; /** * Annotation for linking to entity page from catalog pages. * * @public */ -export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; +export const ANNOTATION_VIEW_URL = 'backstage.io/view-url'; /** * Annotation for linking to entity edit page from catalog pages. * * @public */ -export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; +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..3a346b217c 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -15,10 +15,9 @@ */ export { - EDIT_URL_ANNOTATION, - ENTITY_DEFAULT_NAMESPACE, - ENTITY_META_GENERATED_FIELDS, - VIEW_URL_ANNOTATION, + DEFAULT_NAMESPACE, + ANNOTATION_EDIT_URL, + ANNOTATION_VIEW_URL, } from './constants'; export type { AlphaEntity, @@ -26,7 +25,6 @@ export type { EntityLink, EntityMeta, EntityRelation, - EntityRelationSpec, } from './Entity'; export type { EntityEnvelope } from './EntityEnvelope'; export type { @@ -36,17 +34,8 @@ export type { } from './EntityStatus'; export * from './policies'; export { - compareEntityToRef, getEntityName, parseEntityName, parseEntityRef, - serializeEntityRef, stringifyEntityRef, } from './ref'; -export type { EntityRefContext } from './ref'; -export { - entityHasChanges, - generateEntityEtag, - generateEntityUid, - generateUpdatedEntity, -} from './util'; diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts index 55188d84c8..b92abb0fc2 100644 --- a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts @@ -15,7 +15,7 @@ */ import yaml from 'yaml'; -import { ENTITY_DEFAULT_NAMESPACE } from '../constants'; +import { DEFAULT_NAMESPACE } from '../constants'; import { DefaultNamespaceEntityPolicy } from './DefaultNamespaceEntityPolicy'; describe('DefaultNamespaceEntityPolicy', () => { @@ -57,7 +57,7 @@ describe('DefaultNamespaceEntityPolicy', () => { expect.objectContaining({ metadata: { name: 'my-component-yay', - namespace: ENTITY_DEFAULT_NAMESPACE, + namespace: DEFAULT_NAMESPACE, }, }), ); diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts index 9511dfb05f..5f6efc03dc 100644 --- a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts @@ -16,7 +16,7 @@ import lodash from 'lodash'; import { EntityPolicy } from './types'; -import { ENTITY_DEFAULT_NAMESPACE } from '../constants'; +import { DEFAULT_NAMESPACE } from '../constants'; import { Entity } from '../Entity'; /** @@ -27,7 +27,7 @@ import { Entity } from '../Entity'; export class DefaultNamespaceEntityPolicy implements EntityPolicy { private readonly namespace: string; - constructor(namespace: string = ENTITY_DEFAULT_NAMESPACE) { + constructor(namespace: string = DEFAULT_NAMESPACE) { this.namespace = namespace; } diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index cf4bf0d869..045c4710dc 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -14,14 +14,8 @@ * limitations under the License. */ -import { ENTITY_DEFAULT_NAMESPACE } from './constants'; -import { Entity } from './Entity'; -import { - compareEntityToRef, - parseEntityName, - parseEntityRef, - serializeEntityRef, -} from './ref'; +import { DEFAULT_NAMESPACE } from './constants'; +import { parseEntityName, parseEntityRef } from './ref'; describe('ref', () => { describe('parseEntityName', () => { @@ -34,7 +28,7 @@ describe('ref', () => { expect(() => parseEntityName('b/c')).toThrow(/kind/); expect(parseEntityName('a:c')).toEqual({ kind: 'a', - namespace: ENTITY_DEFAULT_NAMESPACE, + namespace: DEFAULT_NAMESPACE, name: 'c', }); expect(() => parseEntityName('c')).toThrow(/kind/); @@ -123,7 +117,7 @@ describe('ref', () => { ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); expect(parseEntityName('a:c', { defaultKind: 'x' })).toEqual({ kind: 'a', - namespace: ENTITY_DEFAULT_NAMESPACE, + namespace: DEFAULT_NAMESPACE, name: 'c', }); expect( @@ -131,7 +125,7 @@ describe('ref', () => { ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); expect(parseEntityName('c', { defaultKind: 'x' })).toEqual({ kind: 'x', - namespace: ENTITY_DEFAULT_NAMESPACE, + namespace: DEFAULT_NAMESPACE, name: 'c', }); }); @@ -157,7 +151,7 @@ describe('ref', () => { ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); expect( parseEntityName({ kind: 'a', name: 'c' }, { defaultKind: 'x' }), - ).toEqual({ kind: 'a', namespace: ENTITY_DEFAULT_NAMESPACE, name: 'c' }); + ).toEqual({ kind: 'a', namespace: DEFAULT_NAMESPACE, name: 'c' }); expect( parseEntityName( { name: 'c' }, @@ -166,7 +160,7 @@ describe('ref', () => { ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); expect(parseEntityName({ name: 'c' }, { defaultKind: 'x' })).toEqual({ kind: 'x', - namespace: ENTITY_DEFAULT_NAMESPACE, + namespace: DEFAULT_NAMESPACE, name: 'c', }); // empty strings are errors, not defaults @@ -192,19 +186,9 @@ describe('ref', () => { namespace: 'b', name: 'c', }); - expect(parseEntityRef('b/c')).toEqual({ - kind: undefined, - namespace: 'b', - name: 'c', - }); expect(parseEntityRef('a:c')).toEqual({ kind: 'a', - namespace: undefined, - name: 'c', - }); - expect(parseEntityRef('c')).toEqual({ - kind: undefined, - namespace: undefined, + namespace: 'default', name: 'c', }); }); @@ -328,378 +312,4 @@ describe('ref', () => { ).toThrow(/namespace/); }); }); - - 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', - kind: 'K', - metadata: { - name: 'n', - namespace: 'ns', - }, - }; - const entityWithoutNamespace: Entity = { - apiVersion: 'a', - kind: 'K', - metadata: { - name: 'n', - }, - }; - - it('handles matching string refs', () => { - expect(compareEntityToRef(entityWithNamespace, 'K:ns/n')).toBe(true); - expect(compareEntityToRef(entityWithNamespace, 'k:nS/N')).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'K:n', { - defaultNamespace: 'ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'K:n', { - defaultNamespace: 'Ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'ns/n', { defaultKind: 'K' }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'n', { - defaultKind: 'K', - defaultNamespace: 'ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'N', { - defaultKind: 'k', - defaultNamespace: 'nS', - }), - ).toBe(true); - - expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe( - true, - ); - expect(compareEntityToRef(entityWithoutNamespace, 'K:deFault/n')).toBe( - true, - ); - expect( - compareEntityToRef(entityWithoutNamespace, 'K:n', { - defaultNamespace: 'default', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'K:n', { - defaultNamespace: 'deFault', - }), - ).toBe(true); - expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe( - true, - ); - expect(compareEntityToRef(entityWithoutNamespace, 'K:n')).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'default/n', { - defaultKind: 'K', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'n', { - defaultKind: 'K', - defaultNamespace: 'default', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'n', { - defaultKind: 'K', - }), - ).toBe(true); - }); - - it('handles mismatching string refs', () => { - expect(compareEntityToRef(entityWithNamespace, 'X:ns/n')).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, 'ns/n', { - defaultKind: 'X', - }), - ).toBe(false); - - expect(compareEntityToRef(entityWithNamespace, 'K:xx/n')).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, 'K:n', { - defaultNamespace: 'xx', - }), - ).toBe(false); - - expect(compareEntityToRef(entityWithNamespace, 'K:ns/x')).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, 'x', { - defaultKind: 'K', - defaultNamespace: 'ns', - }), - ).toBe(false); - }); - - it('handles matching compound refs', () => { - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'K', - namespace: 'ns', - name: 'n', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'k', - namespace: 'Ns', - name: 'N', - }), - ).toBe(true); - expect( - compareEntityToRef( - entityWithNamespace, - { kind: 'K', name: 'n' }, - { - defaultNamespace: 'ns', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithNamespace, - { namespace: 'ns', name: 'n' }, - { defaultKind: 'K' }, - ), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'n', { - defaultKind: 'K', - defaultNamespace: 'ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'N', { - defaultKind: 'k', - defaultNamespace: 'nS', - }), - ).toBe(true); - - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'K', - namespace: 'default', - name: 'n', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'k', - namespace: 'deFault', - name: 'N', - }), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { kind: 'K', name: 'n' }, - { - defaultNamespace: 'default', - }, - ), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, { kind: 'K', name: 'n' }), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { namespace: 'default', name: 'n' }, - { - defaultKind: 'K', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { name: 'n' }, - { - defaultKind: 'K', - defaultNamespace: 'default', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { name: 'N' }, - { - defaultKind: 'k', - defaultNamespace: 'defAult', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { name: 'n' }, - { - defaultKind: 'K', - }, - ), - ).toBe(true); - }); - - it('handles mismatching compound refs', () => { - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'X', - namespace: 'ns', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithNamespace, - { - namespace: 'ns', - name: 'n', - }, - { defaultKind: 'X' }, - ), - ).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'X', - namespace: 'default', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithoutNamespace, - { - namespace: 'default', - name: 'n', - }, - { defaultKind: 'X' }, - ), - ).toBe(false); - - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'K', - namespace: 'xx', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithNamespace, - { - kind: 'K', - name: 'n', - }, - { defaultNamespace: 'xx' }, - ), - ).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'K', - namespace: 'xx', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithoutNamespace, - { - kind: 'K', - name: 'n', - }, - { defaultNamespace: 'xx' }, - ), - ).toBe(false); - - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'K', - namespace: 'ns', - name: 'x', - }), - ).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'K', - namespace: 'default', - name: 'x', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithoutNamespace, - { - kind: 'K', - name: 'x', - }, - { defaultNamespace: 'default' }, - ), - ).toBe(false); - }); - }); }); diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 958f750e32..f9bfcf38cd 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { EntityName, EntityRef } from '../types'; -import { ENTITY_DEFAULT_NAMESPACE } from './constants'; +import { DEFAULT_NAMESPACE } from './constants'; +import { EntityName } from '../types'; import { Entity } from './Entity'; function parseRefString(ref: string): { @@ -48,23 +48,11 @@ function parseRefString(ref: string): { export function getEntityName(entity: Entity): EntityName { return { kind: entity.kind, - namespace: entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE, + namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, name: entity.metadata.name, }; } -/** - * The context of defaults that entity reference parsing happens within. - * - * @public - */ -export type EntityRefContext = { - /** The default kind, if none is given in the reference */ - defaultKind?: string; - /** The default namespace, if none is given in the reference */ - defaultNamespace?: string; -}; - /** * Parses an entity reference, either on string or compound form, and always * returns a complete entity name including kind, namespace and name. @@ -75,17 +63,23 @@ export type EntityRefContext = { * otherwise specified as part of the options, and will throw an error if no * kind was specified in the input reference and no default kind was given. * + * @deprecated Please use parseEntityRef instead * @public * @param ref - The reference to parse * @param context - The context of defaults that the parsing happens within * @returns A complete entity name */ export function parseEntityName( - ref: EntityRef, - context: EntityRefContext = {}, + ref: string | { kind?: string; namespace?: string; name: string }, + context: { + /** The default kind, if none is given in the reference */ + defaultKind?: string; + /** The default namespace, if none is given in the reference */ + defaultNamespace?: string; + } = {}, ): EntityName { const { kind, namespace, name } = parseEntityRef(ref, { - defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + defaultNamespace: DEFAULT_NAMESPACE, ...context, }); @@ -113,123 +107,52 @@ export function parseEntityName( * @returns The compound form of the reference */ export function parseEntityRef( - ref: EntityRef, - context?: { defaultKind: string; defaultNamespace: string }, -): EntityName; -/** - * parseEntityRef with optional Kind. - * - * @public - */ -export function parseEntityRef( - ref: EntityRef, - context?: { defaultKind: string }, -): { - kind: string; - namespace?: string; - name: string; -}; -/** - * parseEntityRef with optional Namespace. - * - * @public - */ -export function parseEntityRef( - ref: EntityRef, - context?: { defaultNamespace: string }, -): { - kind?: string; - namespace: string; - name: string; -}; -/** - * parseEntityRef with optional Kind and Namespace. - * - * @public - */ -export function parseEntityRef( - ref: EntityRef, - context: EntityRefContext = {}, -): { - kind?: string; - namespace?: string; - name: string; -} { + ref: string | { kind?: string; namespace?: string; name: string }, + context?: { + /** The default kind, if none is given in the reference */ + defaultKind?: string; + /** The default namespace, if none is given in the reference */ + defaultNamespace?: string; + }, +): EntityName { if (!ref) { throw new Error(`Entity reference must not be empty`); } + const defaultKind = context?.defaultKind; + const defaultNamespace = context?.defaultNamespace || DEFAULT_NAMESPACE; + + let kind: string | undefined; + let namespace: string | undefined; + let name: string | undefined; + if (typeof ref === 'string') { const parsed = parseRefString(ref); - return { - kind: parsed.kind ?? context.defaultKind, - namespace: parsed.namespace ?? context.defaultNamespace, - name: parsed.name, - }; - } - - const { kind, namespace, name } = ref; - if (kind === '') { - throw new Error('Entity reference kinds must not be empty'); - } else if (namespace === '') { - throw new Error('Entity reference namespaces must not be empty'); - } else if (!name) { - throw new Error('Entity references must contain a name'); - } - - return { - kind: kind ?? context.defaultKind, - namespace: namespace ?? context.defaultNamespace, - name, - }; -} - -/** - * 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; + kind = parsed.kind ?? defaultKind; + namespace = parsed.namespace ?? defaultNamespace; + name = parsed.name; } else { - kind = ref.kind; - namespace = ref.namespace; + kind = ref.kind ?? defaultKind; + namespace = ref.namespace ?? defaultNamespace; name = ref.name; } - if ( - kind?.includes(':') || - kind?.includes('/') || - namespace?.includes(':') || - namespace?.includes('/') || - name.includes(':') || - name.includes('/') - ) { - return { kind, namespace, name }; + if (!kind) { + const textual = JSON.stringify(ref); + throw new Error( + `Entity reference ${textual} had missing or empty kind (e.g. did not start with "component:" or similar)`, + ); + } else if (!namespace) { + const textual = JSON.stringify(ref); + throw new Error( + `Entity reference ${textual} had missing or empty namespace`, + ); + } else if (!name) { + const textual = JSON.stringify(ref); + throw new Error(`Entity reference ${textual} had missing or empty name`); } - return `${kind ? `${kind}:` : ''}${namespace ? `${namespace}/` : ''}${name}`; + return { kind, namespace, name }; } /** @@ -255,11 +178,11 @@ export function stringifyEntityRef( if ('metadata' in ref) { kind = ref.kind; - namespace = ref.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE; + namespace = ref.metadata.namespace ?? DEFAULT_NAMESPACE; name = ref.metadata.name; } else { kind = ref.kind; - namespace = ref.namespace ?? ENTITY_DEFAULT_NAMESPACE; + namespace = ref.namespace ?? DEFAULT_NAMESPACE; name = ref.name; } @@ -267,58 +190,3 @@ export function stringifyEntityRef( 'en-US', )}/${name.toLocaleLowerCase('en-US')}`; } - -/** - * Compares an entity to either a string reference or a compound reference. - * - * @remarks - * - * The comparison is case insensitive, and all of kind, namespace, and name - * must match (after applying the optional context to the ref). - * - * @public - * @param entity - The entity to match - * @param ref - A string or compound entity ref - * @param context - An optional context of default kind and namespace, that apply - * to the ref if given - * @returns True if matching, false otherwise - */ -export function compareEntityToRef( - entity: Entity, - ref: EntityRef | EntityName, - context?: EntityRefContext, -): boolean { - const entityKind = entity.kind; - const entityNamespace = entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE; - const entityName = entity.metadata.name; - - let refKind: string | undefined; - let refNamespace: string | undefined; - let refName: string; - if (typeof ref === 'string') { - const parsed = parseRefString(ref); - refKind = parsed.kind || context?.defaultKind; - refNamespace = - parsed.namespace || context?.defaultNamespace || ENTITY_DEFAULT_NAMESPACE; - refName = parsed.name; - } else { - refKind = ref.kind || context?.defaultKind; - refNamespace = - ref.namespace || context?.defaultNamespace || ENTITY_DEFAULT_NAMESPACE; - refName = ref.name; - } - - if (!refKind || !refNamespace) { - throw new Error( - `Entity reference or context did not contain kind and namespace`, - ); - } - - return ( - entityKind.toLocaleLowerCase('en-US') === - refKind.toLocaleLowerCase('en-US') && - entityNamespace.toLocaleLowerCase('en-US') === - refNamespace.toLocaleLowerCase('en-US') && - entityName.toLocaleLowerCase('en-US') === refName.toLocaleLowerCase('en-US') - ); -} diff --git a/packages/catalog-model/src/entity/util.test.ts b/packages/catalog-model/src/entity/util.test.ts deleted file mode 100644 index 1c39961c0f..0000000000 --- a/packages/catalog-model/src/entity/util.test.ts +++ /dev/null @@ -1,198 +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 lodash from 'lodash'; -import { - generateEntityEtag, - generateEntityUid, - entityHasChanges, - generateUpdatedEntity, -} from './util'; -import { Entity } from './Entity'; - -describe('util', () => { - describe('generateEntityUid', () => { - it('generates randomness', () => { - expect(generateEntityUid()).not.toEqual(''); - expect(generateEntityUid()).not.toEqual(generateEntityUid()); - }); - }); - - describe('generateEntityEtag', () => { - it('generates randomness', () => { - expect(generateEntityEtag()).not.toEqual(''); - expect(generateEntityEtag()).not.toEqual(generateEntityEtag()); - }); - }); - - describe('entityHasChanges', () => { - let a: Entity; - beforeEach(() => { - a = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'name', - custom: 'custom', - labels: { - labelKey: 'labelValue', - }, - annotations: { - annotationKey: 'annotationValue', - }, - }, - spec: { - a: 'a', - }, - }; - }); - - it('happy path: clone has no changes', () => { - const b = lodash.cloneDeep(a); - expect(entityHasChanges(a, b)).toBe(false); - }); - - it('detects root field changes', () => { - let b: any = lodash.cloneDeep(a); - b.apiVersion += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.apiVersion; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.kind += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.kind; - expect(entityHasChanges(a, b)).toBe(true); - }); - - it('detects metadata changes', () => { - let b: any = lodash.cloneDeep(a); - b.metadata.name += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.metadata.custom; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.metadata.custom; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.metadata.labels.n = 'n'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.metadata.labels.labelKey += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.metadata.annotations.annotationKey += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.metadata.annotations.annotationKey; - expect(entityHasChanges(a, b)).toBe(true); - }); - - it('detects spec changes', () => { - let b: any = lodash.cloneDeep(a); - b.spec.a += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.spec.a; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.spec.n = 'n'; - expect(entityHasChanges(a, b)).toBe(true); - }); - }); - - describe('generateUpdatedEntity', () => { - let a: Entity; - let b: any; - beforeEach(() => { - a = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - uid: 'da921f56-f655-4e6e-9b8b-bb19a57818d8', - etag: 'NzY5NDA5NzQtYmEwNC00MDY0LWFiYmItNTYxYzQxM2JhZDcx', - generation: 2, - name: 'name', - custom: 'custom', - labels: { - labelKey: 'labelValue', - }, - annotations: { - annotationKey: 'annotationValue', - }, - }, - spec: { - a: 'a', - }, - }; - b = lodash.cloneDeep(a); - delete b.metadata.uid; - delete b.metadata.etag; - delete b.metadata.generation; - }); - - it('happy path: running on itself leaves it unchanged', () => { - const result = generateUpdatedEntity(a, b); - expect(result).toEqual(a); - }); - - it('bumps etag and generation when spec is changed', () => { - b.spec.a += 'a'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation! + 1); - expect(result.spec).toEqual({ a: 'aa' }); - }); - - it('bumps only etag when other things than spec are changed', () => { - b.metadata.n = 'n'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation); - expect(result.metadata.n).toEqual('n'); - }); - - it('retains new annotations', () => { - b.metadata.annotations.annotationKey = 'changedValue'; - b.metadata.annotations.newKey = 'newValue'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation); - expect(result.metadata.annotations).toEqual({ - annotationKey: 'changedValue', - newKey: 'newValue', - }); - }); - - it('retains old annotations', () => { - b.metadata.annotations.newKey = 'newValue'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation); - expect(result.metadata.annotations).toEqual({ - annotationKey: 'annotationValue', - newKey: 'newValue', - }); - }); - }); -}); diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts deleted file mode 100644 index 4506a8d852..0000000000 --- a/packages/catalog-model/src/entity/util.ts +++ /dev/null @@ -1,129 +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 lodash from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; -import { Entity, AlphaEntity } from './Entity'; - -/** - * Generates a new random UID for an entity. - * - * @public - * @returns A string with enough randomness to uniquely identify an entity - */ -export function generateEntityUid(): string { - return uuidv4(); -} - -/** - * Generates a new random Etag for an entity. - * - * @public - * @returns A string with enough randomness to uniquely identify an entity - * revision - */ -export function generateEntityEtag(): string { - return Buffer.from(uuidv4(), 'utf8').toString('base64').replace(/[^\w]/g, ''); -} - -/** - * Checks whether there are any significant changes going from the previous to - * the next version of this entity. - * - * @remarks - * - * Significance, in this case, means that we do not compare generated fields - * such as uid, etag and generation. - * - * Note that this comparison does NOT take status, relations or similar into - * account. It only compares the actual input entity data, i.e. metadata and - * spec. - * - * @public - * @param previous - The old state of the entity - * @param next - The new state of the entity - */ -export function entityHasChanges(previous: Entity, next: Entity): boolean { - const e1 = lodash.cloneDeep(previous); - const e2 = lodash.cloneDeep(next); - - if (!e1.metadata.labels) { - e1.metadata.labels = {}; - } - if (!e2.metadata.labels) { - e2.metadata.labels = {}; - } - if (!e1.metadata.annotations) { - e1.metadata.annotations = {}; - } - if (!e2.metadata.annotations) { - e2.metadata.annotations = {}; - } - if (!e1.metadata.tags) { - e1.metadata.tags = []; - } - if (!e2.metadata.tags) { - e2.metadata.tags = []; - } - - // Remove generated fields - delete e1.metadata.uid; - delete e1.metadata.etag; - delete e1.metadata.generation; - delete e2.metadata.uid; - delete e2.metadata.etag; - delete e2.metadata.generation; - - // Remove things that we explicitly do not compare - delete e1.relations; - delete (e1 as AlphaEntity).status; - delete e2.relations; - delete (e2 as AlphaEntity).status; - - return !lodash.isEqual(e1, e2); -} - -/** - * Takes an old revision of an entity and a new desired state, and merges - * them into a complete new state. - * - * @remarks - * - * The previous revision is expected to be a complete model loaded from the - * catalog, including the uid, etag and generation fields. - * - * @public - * @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 - */ -export function generateUpdatedEntity(previous: Entity, next: Entity): Entity { - const { uid, etag, generation } = previous.metadata; - if (!uid || !etag || !generation) { - throw new Error('Previous entity must have uid, etag and generation'); - } - - const result = lodash.cloneDeep(next); - - // Generated fields are copied and updated - const bumpEtag = entityHasChanges(previous, result); - const bumpGeneration = !lodash.isEqual(previous.spec, result.spec); - result.metadata.uid = uid; - result.metadata.etag = bumpEtag ? generateEntityEtag() : etag; - result.metadata.generation = bumpGeneration ? generation + 1 : generation; - - return result; -} diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index ef55337ae2..986341bd33 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -24,5 +24,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityName, EntityRef, JSONSchema } from './types'; +export type { EntityName, EntityRef } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index be9f7a0d6a..7211c25a8d 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -50,8 +50,6 @@ export type { SystemEntityV1alpha1 as SystemEntity, SystemEntityV1alpha1, } from './SystemEntityV1alpha1'; -export { templateEntityV1beta2Validator } from './TemplateEntityV1beta2'; -export type { TemplateEntityV1beta2 } from './TemplateEntityV1beta2'; export type { KindValidator } from './types'; export { userEntityV1alpha1Validator } from './UserEntityV1alpha1'; export type { diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 8eb393f555..61dbc0b305 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -18,16 +18,16 @@ * Constant storing location annotation. * * @public */ -export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; +export const ANNOTATION_LOCATION = 'backstage.io/managed-by-location'; /** * Constant storing origin location annotation * * @public */ -export const ORIGIN_LOCATION_ANNOTATION = +export const ANNOTATION_ORIGIN_LOCATION = 'backstage.io/managed-by-origin-location'; /** * Contant storing source location annotation * * @public */ -export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; +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..8f8acc3b1d 100644 --- a/packages/catalog-model/src/location/helpers.ts +++ b/packages/catalog-model/src/location/helpers.ts @@ -14,48 +14,46 @@ * 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. * - * 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 reference, e.g. `'url:https://host'` - * @returns A location reference, e.g. `{ type: 'url', target: 'https://host' }` + * @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 parseLocationReference(ref: string): { +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}'`, ); } @@ -63,27 +61,22 @@ export function parseLocationReference(ref: string): { } /** - * Turns a location reference 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. + * Turns a location ref into its string form. * * @public - * @param ref - A location reference, e.g. `{ type: 'url', target: 'https://host' }` - * @returns A string-form location reference, e.g. `'url:https://host'` + * @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 stringifyLocationReference(ref: { +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 +98,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 +107,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 624c1acd1d..dc709144b2 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -15,13 +15,13 @@ */ export { - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, - SOURCE_LOCATION_ANNOTATION, + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, + ANNOTATION_SOURCE_LOCATION, } from './annotation'; export { - parseLocationReference, - stringifyLocationReference, getEntitySourceLocation, + parseLocationRef, + stringifyLocationRef, } from './helpers'; -export type { Location, LocationSpec } from './types'; +export type { LocationSpec } from './types'; diff --git a/packages/catalog-model/src/location/types.ts b/packages/catalog-model/src/location/types.ts index 725f873f2d..9e2c9339e6 100644 --- a/packages/catalog-model/src/location/types.ts +++ b/packages/catalog-model/src/location/types.ts @@ -24,18 +24,10 @@ * default value: 'required'. * * @public + * @deprecated Import from `@backstage/plugin-catalog-backend` instead. */ export type LocationSpec = { type: string; target: string; presence?: 'optional' | 'required'; }; - -/** - * Entity location for a specific entity. - * - * @public - */ -export type Location = { - id: string; -} & LocationSpec; diff --git a/packages/catalog-model/src/schema/EntityMeta.schema.json b/packages/catalog-model/src/schema/EntityMeta.schema.json index c834bfb113..7b2b9d3f82 100644 --- a/packages/catalog-model/src/schema/EntityMeta.schema.json +++ b/packages/catalog-model/src/schema/EntityMeta.schema.json @@ -38,7 +38,8 @@ "type": "integer", "description": "A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations.", "examples": [1], - "minimum": 1 + "minimum": 1, + "deprecated": true }, "name": { "type": "string", diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 1a0fab267c..5b5f063ca3 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -14,16 +14,6 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/types'; -import { JSONSchema7 } from 'json-schema'; - -/** - * JSONSchema extendable by arbitrary JSON attributes - * - * @public - */ -export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; - /** * A complete entity name, with the full kind-namespace-name triplet. * @@ -39,6 +29,7 @@ export type EntityName = { * A reference by name to an entity, either as a compact string representation, * or as a compound reference structure. * + * @deprecated Please use string directly, or EntityName (depending on what you actually need) * @remarks * * The string representation is on the form `[:][/]`. diff --git a/packages/cli-common/CHANGELOG.md b/packages/cli-common/CHANGELOG.md index e588c02edf..c890e44b29 100644 --- a/packages/cli-common/CHANGELOG.md +++ b/packages/cli-common/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/cli-common +## 0.1.8 + +### Patch Changes + +- Fix for the previous release with missing type declarations. + +## 0.1.7 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` + ## 0.1.6 ### Patch Changes diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index cdb4b01b6d..3b05e93603 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli-common", "description": "Common functionality used by cli, backend, and create-app", - "version": "0.1.6", + "version": "0.1.8", "private": false, "main": "src/index.ts", "types": "src/index.ts", @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "node-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,12 +24,13 @@ ], "license": "Apache-2.0", "scripts": { - "build": "backstage-cli build --outputs cjs,types", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "devDependencies": { "@types/jest": "^26.0.7", diff --git a/packages/cli/.snyk b/packages/cli/.snyk index 3ad43bd3de..0a32a6ac12 100644 --- a/packages/cli/.snyk +++ b/packages/cli/.snyk @@ -3,80 +3,65 @@ version: v1.22.1 # ignores vulnerabilities until expiry date; change duration by modifying expiry date ignore: SNYK-JS-ANSIHTML-1296849: - - 'webpack-dev-server > ansi-html': + - webpack-dev-server > ansi-html: reason: Developer tools are not a valid target for ReDoS attacks expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z - SNYK-JS-ANSIREGEX-1583908: - '*': reason: Developer tools are not a valid target for ReDoS attacks expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z - SNYK-JS-CSSWHAT-1298035: - '*': reason: Developer tools are not a valid target for ReDoS attacks expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z - SNYK-JS-BROWSERSLIST-1090194: - '*': reason: Developer tools are not a valid target for ReDoS attacks expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z - SNYK-JS-ISSVG-1243891: - '*': reason: Developer tools are not a valid target for ReDoS attacks expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z - SNYK-JS-ISSVG-1085627: - '*': reason: Developer tools are not a valid target for ReDoS attacks expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z - SNYK-JS-POSTCSS-1255640: - '*': reason: Developer tools are not a valid target for ReDoS attacks expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z - SNYK-JS-POSTCSS-1090595: - '*': reason: Developer tools are not a valid target for ReDoS attacks expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z - SNYK-JS-WS-1296835: - '*': reason: Developer tools are not a valid target for ReDoS attacks expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z - - SNYK-JS-IMMER-1540542: - - '*': - reason: Prototype pollution is not an effective attack against a CLI as it already executes arbitrary code - expires: 2022-03-06T17:18:55.019Z - created: 2021-09-06T17:18:55.027Z - - SNYK-JS-SETVALUE-1540541: - - '*': - reason: Prototype pollution is not an effective attack against a CLI as it already executes arbitrary code - expires: 2022-03-06T17:18:55.019Z - created: 2021-09-06T17:18:55.027Z - 'snyk:lic:npm:rollup-plugin-dts:LGPL-3.0': - '*': - reason: Backstage itself does not redistribute this dependency in minified form + reason: This is a development dependency only and is therefore acceptable expires: 2031-09-06T17:18:55.027Z created: 2021-09-06T17:18:55.027Z - 'snyk:lic:npm:axe-core:MPL-2.0': - '*': - reason: Backstage itself does not redistribute this dependency in minified form + reason: This is a development dependency only and is therefore acceptable expires: 2031-09-06T17:18:55.027Z created: 2021-09-06T17:18:55.027Z + SNYK-JS-UNSETVALUE-2400660: + - '*': + reason: >- + The pollution in this vulnerability does not allow injection of + arbitrary values and is therefore a negligible risk for a CLI. + expires: 2022-08-17T21:54:43.658Z + created: 2022-02-15T21:54:43.668Z patch: {} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 37bea1e016..46b67615c4 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/cli +## 0.14.1 + +### Patch Changes + +- 5cc7f48400: Fixed a bug in the built-in Jest configuration that prevented it from identifying packages that had migrated to using the new package scripts to run tests. +- 49ae6c9573: chore(deps-dev): bump `@types/rollup-plugin-postcss` from 2.0.1 to 3.1.4 +- d64b8d3678: chore(deps): bump `minimatch` from 3.0.4 to 5.0.0 +- d2e9d2a34f: chore(deps): bump `@hot-loader/react-dom` from 16.13.0 to 17.0.2 +- c2f3a548cf: Fix building of backends with `repo build --all`, where it would previously only work if the build was executed within the backend package. +- 3d7ed5377a: Ignore setupTests and the file inside ./dev folder recursively. Eslint + can not resolve relative paths as we defined in the rule import/no-extraneous-dependencies, and it does not apply this rule. + + A downside to use a recursive definition would be to checking all `dev` folders, which might not be wanted. Ensure you don't use + the `dev` folder out of scope (must be used for dev. env. only) + +- Updated dependencies + - @backstage/config-loader@0.9.6 + +## 0.14.0 + +### Minor Changes + +- 1fc0cd3896: Bumped Webpack and Rollup `svgr` dependencies and updated the AST template for `.icon.svg` modules. This means that SVG icon imports are now using SVGO v2. + +### Patch Changes + +- 0b74c72987: Added a new experimental and hidden `backstage-cli repo lint` command that can be used to lint all packages in the project, similar to `lerna run lint`. +- 532dae9c4c: The `versions:bump --release next` command is updated to compare the `main` and `next` release manifests and prefer the latest. +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 22862486de: 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. +- 22862486de: 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. +- 7410e12268: Several changes were made to the new experimental package roles system. Unless you have been experimenting with using this new system, these changes have no affect on your project. + + Renamed the `backstage-cli migrate package-role` command to `backstage-cli migrate package-roles`. + + Updated the package role definitions by renaming `app` to `frontend`, `plugin-frontend` to `frontend-plugin`, `plugin-frontend-module` to `frontend-plugin-module`, `plugin-backend` to `backend-plugin`, and `plugin-backend-module` to `backend-plugin-module` + + The `backstage-cli migrate package-scripts` received several tweaks to make it more accurate. It now tries to maintain existing script arguments, like `--config` parameters for `build` and `start` scripts. + + The `script` command category has been renamed to `package`. + + The `backstage-cli package build` command set an incorrect target directory for `app` and `backend` packages, which has been fixed. + + The `backend:bundle` and `repo build` command for the `backend` role was previously ignoring building of bundled packages that had migrated to use package roles and the standard build script, this has now been fixed. + +- c6bbafb516: 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. +- 7410e12268: The `test` command now automatically adds `--passWithNoTests` to the Jest invocation. To revert this behavior, pass `--passWithNoTests=false` or `--no-passWithNoTests`. +- Updated dependencies + - @backstage/config-loader@0.9.4 + - @backstage/errors@0.2.1 + - @backstage/release-manifests@0.0.2 + - @backstage/cli-common@0.1.7 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.13.2 ### Patch Changes diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 06f22933e8..5555dad451 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -55,8 +55,8 @@ module.exports = { devDependencies: [ '**/*.test.*', '**/*.stories.*', - 'src/setupTests.*', - 'dev/**', + '**/src/setupTests.*', + '**/dev/**', ], optionalDependencies: true, peerDependencies: true, diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 978c598584..d5b85b9748 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'), @@ -174,7 +175,10 @@ async function getRootConfig() { // script to determine whether a given package should be tested const packageData = await fs.readJson(packagePath); const testScript = packageData.scripts && packageData.scripts.test; - if (testScript && testScript.includes('backstage-cli test')) { + const isSupportedTestScript = + testScript?.includes('backstage-cli test') || + testScript?.includes('backstage-cli package test'); + if (testScript && isSupportedTestScript) { return await getProjectConfig(projectPath, packageData.name); } 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 589ac58646..e5551ce29e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,11 +1,14 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.13.2", + "version": "0.14.1", "private": false, "publishConfig": { "access": "public" }, + "backstage": { + "role": "cli" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -18,23 +21,24 @@ "license": "Apache-2.0", "main": "dist/index.cjs.js", "scripts": { - "build": "backstage-cli build --outputs cjs", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "clean": "backstage-cli clean", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", "start": "nodemon --" }, "bin": { "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@backstage/cli-common": "^0.1.6", - "@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", + "@backstage/cli-common": "^0.1.8", + "@backstage/config": "^0.1.15", + "@backstage/config-loader": "^0.9.6", + "@backstage/errors": "^0.2.2", + "@backstage/release-manifests": "^0.0.2", + "@backstage/types": "^0.1.3", + "@hot-loader/react-dom-v16": "npm:@hot-loader/react-dom@^16.0.2", + "@hot-loader/react-dom-v17": "npm:@hot-loader/react-dom@^17.0.2", "@manypkg/get-packages": "^1.1.3", "@octokit/request": "^5.4.12", "@rollup/plugin-commonjs": "^21.0.1", @@ -48,8 +52,8 @@ "@sucrase/webpack-loader": "^2.0.0", "@svgr/plugin-jsx": "6.2.x", "@svgr/plugin-svgo": "6.2.x", - "@svgr/rollup": "5.5.x", - "@svgr/webpack": "5.5.x", + "@svgr/rollup": "6.2.x", + "@svgr/webpack": "6.2.x", "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/parser": "^5.9.0", @@ -85,7 +89,7 @@ "json-schema": "^0.4.0", "jest-transform-yaml": "^1.0.0", "lodash": "^4.17.21", - "minimatch": "3.0.4", + "minimatch": "5.0.0", "mini-css-extract-plugin": "^2.4.2", "npm-packlist": "^3.0.0", "node-libs-browser": "^2.2.1", @@ -118,14 +122,14 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", - "@backstage/theme": "^0.2.14", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", + "@backstage/theme": "^0.2.15", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", @@ -137,7 +141,8 @@ "@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", + "@types/rollup-plugin-postcss": "^3.1.4", + "@types/svgo": "^2.6.2", "@types/tar": "^6.1.1", "@types/terser-webpack-plugin": "^5.0.4", "@types/yarnpkg__lockfile": "^1.1.4", diff --git a/packages/cli/src/commands/build/buildBackend.ts b/packages/cli/src/commands/build/buildBackend.ts index e32b7956db..d7c4f72dbe 100644 --- a/packages/cli/src/commands/build/buildBackend.ts +++ b/packages/cli/src/commands/build/buildBackend.ts @@ -35,7 +35,10 @@ export async function buildBackend(options: BuildBackendOptions) { 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]) }); + await buildPackage({ + targetDir: options.targetDir, + outputs: new Set([Output.cjs]), + }); const tmpDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-bundle')); try { @@ -49,19 +52,20 @@ export async function buildBackend(options: BuildBackendOptions) { // 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); + const distDir = resolvePath(targetDir, 'dist'); + await fs.remove(distDir); + await fs.mkdir(distDir); // 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), + resolvePath(distDir, 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), + file: resolvePath(distDir, BUNDLE_FILE), cwd: tmpDir, portable: true, noMtime: true, diff --git a/packages/cli/src/commands/build/buildApp.ts b/packages/cli/src/commands/build/buildFrontend.ts similarity index 95% rename from packages/cli/src/commands/build/buildApp.ts rename to packages/cli/src/commands/build/buildFrontend.ts index b0959b2d6a..6d7ca2e6a9 100644 --- a/packages/cli/src/commands/build/buildApp.ts +++ b/packages/cli/src/commands/build/buildFrontend.ts @@ -26,7 +26,7 @@ interface BuildAppOptions { configPaths: string[]; } -export async function buildApp(options: BuildAppOptions) { +export async function buildFrontend(options: BuildAppOptions) { const { targetDir, writeStats, configPaths } = options; const { name } = await fs.readJson(resolvePath(targetDir, 'package.json')); await buildBundle({ diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 1189883b12..5c8065dbb4 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -18,22 +18,22 @@ 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 { buildFrontend } from './buildFrontend'; 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'), + if (role === 'frontend') { + return buildFrontend({ + targetDir: paths.targetDir, configPaths: cmd.config as string[], writeStats: Boolean(cmd.stats), }); } if (role === 'backend') { return buildBackend({ - targetDir: paths.resolveTarget('dist'), + targetDir: paths.targetDir, skipBuildDependencies: Boolean(cmd.skipBuildDependencies), }); } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 770d216ba2..d3baa37b3f 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -42,18 +42,36 @@ export function registerRepoCommand(program: CommanderStatic) { 'Build all packages, including bundled app and backend packages.', ) .action(lazy(() => import('./repo/build').then(m => m.command))); + + command + .command('lint') + .description('Lint all packages in the project') + .option( + '--format ', + 'Lint report output format', + 'eslint-formatter-friendly', + ) + .option( + '--since ', + 'Only lint packages that changed since the specified ref', + ) + .option('--fix', 'Attempt to automatically fix violations') + .action(lazy(() => import('./repo/lint').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('package [command]', { hidden: true }) + .description('Lifecycle scripts for individual 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( + '--role ', + 'Run the command with an explicit package role [EXPERIMENTAL]', + ) .option('--check', 'Enable type checking and linting if available') .option('--inspect', 'Enable debugger in Node.js environments') .option( @@ -65,6 +83,10 @@ export function registerScriptCommand(program: CommanderStatic) { command .command('build') .description('Build a package for production deployment or publishing') + .option( + '--role ', + 'Run the command with an explicit package role [EXPERIMENTAL]', + ) .option( '--minify', 'Minify the generated code. Does not apply to app or backend packages.', @@ -89,7 +111,7 @@ export function registerScriptCommand(program: CommanderStatic) { ) .action(lazy(() => import('./build').then(m => m.command))); - program + command .command('lint') .option( '--format ', @@ -100,7 +122,7 @@ export function registerScriptCommand(program: CommanderStatic) { .description('Lint a package') .action(lazy(() => import('./lint').then(m => m.default))); - program + command .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 @@ -129,7 +151,7 @@ export function registerMigrateCommand(program: CommanderStatic) { .description('Migration utilities [EXPERIMENTAL]'); command - .command('package-role') + .command('package-roles') .description(`Add package role field to packages that don't have it`) .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); @@ -142,7 +164,7 @@ export function registerMigrateCommand(program: CommanderStatic) { } export function registerCommands(program: CommanderStatic) { - // TODO(Rugvip): Deprecate in favor of script variant + // TODO(Rugvip): Deprecate in favor of package variant program .command('app:build') .description('Build an app for a production release') @@ -150,7 +172,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 + // TODO(Rugvip): Deprecate in favor of package variant program .command('app:serve') .description('Serve an app for local development') @@ -158,7 +180,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 + // TODO(Rugvip): Deprecate in favor of package variant program .command('backend:build') .description('Build a backend plugin') @@ -166,7 +188,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 + // TODO(Rugvip): Deprecate in favor of package variant program .command('backend:bundle') .description('Bundle the backend into a deployment archive') @@ -176,7 +198,7 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./backend/bundle').then(m => m.default))); - // TODO(Rugvip): Deprecate in favor of script variant + // TODO(Rugvip): Deprecate in favor of package variant program .command('backend:dev') .description('Start local development server with HMR for the backend') @@ -225,7 +247,7 @@ export function registerCommands(program: CommanderStatic) { lazy(() => import('./create-plugin/createPlugin').then(m => m.default)), ); - // TODO(Rugvip): Deprecate in favor of script variant + // TODO(Rugvip): Deprecate in favor of package variant program .command('plugin:build') .description('Build a plugin') @@ -233,7 +255,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 + // TODO(Rugvip): Deprecate in favor of package variant program .command('plugin:serve') .description('Serves the dev/ folder of a plugin') @@ -248,7 +270,7 @@ 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 + // TODO(Rugvip): Deprecate in favor of package variant program .command('build') .description('Build a package for publishing') @@ -257,7 +279,7 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./oldBuild').then(m => m.default))); - // TODO(Rugvip): Deprecate in favor of script variant + // TODO(Rugvip): Deprecate in favor of package variant program .command('lint') .option( @@ -269,7 +291,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 + // TODO(Rugvip): Deprecate in favor of package variant program .command('test') .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args @@ -355,19 +377,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 + // TODO(Rugvip): Deprecate in favor of package 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 + // TODO(Rugvip): Deprecate in favor of package 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 + // TODO(Rugvip): Deprecate in favor of package variant program .command('clean') .description('Delete cache directories') diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index a3fdd20018..c7dab9466b 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -19,6 +19,8 @@ import { resolve as resolvePath } from 'path'; import { PackageGraph } from '../../lib/monorepo'; import { getRoleFromPackage, getRoleInfo, PackageRole } from '../../lib/role'; +const configArgPattern = /--config[=\s][^\s$]+/; + const noStartRoles: PackageRole[] = ['cli', 'common-library']; export async function command() { @@ -33,17 +35,52 @@ export async function command() { const roleInfo = getRoleInfo(role); const hasStart = !noStartRoles.includes(role); - const isBundled = roleInfo.output.includes('bundle'); + const needsPack = !(roleInfo.output.includes('bundle') || role === 'cli'); + const scripts = packageJson.scripts ?? {}; + + const startCmd = ['start']; + if (scripts.start?.includes('--check')) { + startCmd.push('--check'); + } + if (scripts.start?.includes('--config')) { + startCmd.push(...(scripts.start.match(configArgPattern) ?? [])); + } + + const buildCmd = ['build']; + if (scripts.build?.includes('--minify')) { + buildCmd.push('--minify'); + } + if (scripts.build?.includes('--experimental-type-build')) { + buildCmd.push('--experimental-type-build'); + } + if (scripts.build?.includes('--config')) { + buildCmd.push(...(scripts.build.match(configArgPattern) ?? [])); + } + + // For test scripts we keep all existing flags except for --passWithNoTests, since that's now default + const testCmd = ['test']; + if (scripts.test?.startsWith('backstage-cli test')) { + const args = scripts.test + .slice('backstage-cli test'.length) + .split(' ') + .filter(Boolean); + if (args.includes('--passWithNoTests')) { + args.splice(args.indexOf('--passWithNoTests'), 1); + } + testCmd.push(...args); + } 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', + ...(hasStart && { + start: `backstage-cli package ${startCmd.join(' ')}`, + }), + build: `backstage-cli package ${buildCmd.join(' ')}`, + lint: 'backstage-cli package lint', + test: `backstage-cli package ${testCmd.join(' ')}`, + clean: 'backstage-cli package clean', + ...(needsPack && { + postpack: 'backstage-cli package postpack', + prepack: 'backstage-cli package prepack', }), }; @@ -52,7 +89,12 @@ export async function command() { (packageJson.scripts = packageJson.scripts || {}); for (const [name, value] of Object.entries(expectedScripts)) { - if (currentScripts[name] !== value) { + const currentScript = currentScripts[name]; + + const isMissing = !currentScript; + const isDifferent = currentScript !== value; + const isBackstageScript = currentScript?.includes('backstage-cli'); + if (isMissing || (isDifferent && isBackstageScript)) { changed = true; currentScripts[name] = value; } diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index 9c32386304..ceb468ebfa 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -23,7 +23,7 @@ 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 { buildFrontend } from '../build/buildFrontend'; import { buildBackend } from '../build/buildBackend'; function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) { @@ -43,7 +43,9 @@ function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) { } if (!targetCmd) { - throw new Error(`Could not find script command '${commandPath.join(' ')}'`); + throw new Error( + `Could not find package command '${commandPath.join(' ')}'`, + ); } const cmd = targetCmd; @@ -80,7 +82,7 @@ export async function command(cmd: Command): Promise { const apps = new Array(); const backends = new Array(); - const parseBuildScript = createScriptOptionsParser(cmd, ['script', 'build']); + const parseBuildScript = createScriptOptionsParser(cmd, ['package', 'build']); const options = packages.flatMap(pkg => { const role = @@ -90,7 +92,7 @@ export async function command(cmd: Command): Promise { return []; } - if (role === 'app') { + if (role === 'frontend') { apps.push(pkg); return []; } else if (role === 'backend') { @@ -137,7 +139,7 @@ export async function command(cmd: Command): Promise { ); return; } - await buildApp({ + await buildFrontend({ targetDir: pkg.dir, configPaths: (buildOptions.config as string[]) ?? [], writeStats: Boolean(buildOptions.stats), diff --git a/packages/cli/src/commands/repo/lint.ts b/packages/cli/src/commands/repo/lint.ts new file mode 100644 index 0000000000..b054dabe34 --- /dev/null +++ b/packages/cli/src/commands/repo/lint.ts @@ -0,0 +1,113 @@ +/* + * 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 { PackageGraph, ExtendedPackageJSON } from '../../lib/monorepo'; +import { runWorkerQueueThreads } from '../../lib/parallel'; +import { paths } from '../../lib/paths'; + +function depCount(pkg: ExtendedPackageJSON) { + const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; + const devDeps = pkg.devDependencies + ? Object.keys(pkg.devDependencies).length + : 0; + return deps + devDeps; +} + +export async function command(cmd: Command): Promise { + let packages = await PackageGraph.listTargetPackages(); + + if (cmd.since) { + const graph = PackageGraph.fromPackages(packages); + packages = await graph.listChangedPackages({ ref: cmd.since }); + } + + // Packages are ordered from most to least number of dependencies, as a + // very cheap way of guessing which packages are more likely to be larger. + packages.sort((a, b) => depCount(b.packageJson) - depCount(a.packageJson)); + + // 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); + } + + // Make sure lint output is colored unless the user explicitly disabled it + if (!process.env.FORCE_COLOR) { + process.env.FORCE_COLOR = '1'; + } + + const resultsList = await runWorkerQueueThreads({ + items: packages.map(pkg => ({ + fullDir: pkg.dir, + relativeDir: relativePath(paths.targetRoot, pkg.dir), + })), + workerData: { + fix: Boolean(cmd.fix), + format: cmd.format as string | undefined, + }, + workerFactory: async ({ fix, format }) => { + const { ESLint } = require('eslint'); + + return async ({ + fullDir, + relativeDir, + }): Promise<{ relativeDir: string; resultText: string }> => { + // Bit of a hack to make file resolutions happen from the correct directory + // since some lint rules don't respect the cwd of ESLint + process.cwd = () => fullDir; + + const start = Date.now(); + const eslint = new ESLint({ + cwd: fullDir, + fix, + extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'], + }); + const formatter = await eslint.loadFormatter(format); + + const results = await eslint.lintFiles(['.']); + + const count = String(results.length).padStart(3); + const time = ((Date.now() - start) / 1000).toFixed(2); + console.log(`Checked ${count} files in ${relativeDir} ${time}s`); + + if (fix) { + await ESLint.outputFixes(results); + } + + const resultText = formatter.format(results); + + return { relativeDir, resultText }; + }; + }, + }); + + let failed = false; + for (const { relativeDir, resultText } of resultsList) { + if (resultText) { + console.log(); + console.log(chalk.red(`Lint failed in ${relativeDir}:`)); + console.log(resultText.trimLeft()); + + failed = true; + } + } + + if (failed) { + process.exit(1); + } +} diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts index 6615d5fd46..7a11028800 100644 --- a/packages/cli/src/commands/start/command.ts +++ b/packages/cli/src/commands/start/command.ts @@ -31,19 +31,19 @@ export async function command(cmd: Command): Promise { switch (role) { case 'backend': - case 'plugin-backend': - case 'plugin-backend-module': + case 'backend-plugin': + case 'backend-plugin-module': case 'node-library': return startBackend(options); - case 'app': + case 'frontend': return startFrontend({ ...options, entry: 'src/index', verifyVersions: true, }); case 'web-library': - case 'plugin-frontend': - case 'plugin-frontend-module': + case 'frontend-plugin': + case 'frontend-plugin-module': return startFrontend({ entry: 'dev/index', ...options }); default: throw new Error( diff --git a/packages/cli/src/commands/testCommand.ts b/packages/cli/src/commands/testCommand.ts index 24a4e06f9e..7165b20925 100644 --- a/packages/cli/src/commands/testCommand.ts +++ b/packages/cli/src/commands/testCommand.ts @@ -29,7 +29,11 @@ function includesAnyOf(hayStack: string[], ...needles: string[]) { export default async (cmd: Command) => { // all args are forwarded to jest - const rawArgs = cmd.parent.rawArgs as string[]; + let parent = cmd; + while (parent.parent) { + parent = parent.parent; + } + const rawArgs = parent.rawArgs as string[]; const args = rawArgs.slice(rawArgs.indexOf('test') + 1); // Only include our config if caller isn't passing their own config @@ -37,6 +41,10 @@ export default async (cmd: Command) => { args.push('--config', paths.resolveOwn('config/jest.js')); } + if (!includesAnyOf(args, '--no-passWithNoTests', '--passWithNoTests=false')) { + args.push('--passWithNoTests'); + } + // Run in watch mode unless in CI, coverage mode, or running all tests if ( !process.env.CI && diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 3cfcafc005..bfd05807ad 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -15,7 +15,6 @@ */ import fs from 'fs-extra'; -import chalk from 'chalk'; import { resolve as resolvePath } from 'path'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; @@ -118,43 +117,11 @@ export async function createConfig( }), ); - const appParamDeprecationMsg = chalk.red( - 'DEPRECATION WARNING: using `app.` in the index.html template is deprecated, use `config.getString("app.")` instead.', - ); plugins.push( new HtmlWebpackPlugin({ template: paths.targetHtml, templateParameters: { publicPath: validBaseUrl.pathname.replace(/\/$/, ''), - app: { - get title() { - console.warn(appParamDeprecationMsg); - return frontendConfig.getString('app.title'); - }, - get baseUrl() { - console.warn(appParamDeprecationMsg); - return validBaseUrl.href; - }, - get googleAnalyticsTrackingId() { - console.warn(appParamDeprecationMsg); - return frontendConfig.getOptionalString( - 'app.googleAnalyticsTrackingId', - ); - }, - get datadogRum() { - console.warn(appParamDeprecationMsg); - return { - env: frontendConfig.getOptionalString('app.datadogRum.env'), - clientToken: frontendConfig.getOptionalString( - 'app.datadogRum.clientToken', - ), - applicationId: frontendConfig.getOptionalString( - 'app.datadogRum.applicationId', - ), - site: frontendConfig.getOptionalString('app.datadogRum.site'), - }; - }, - }, config: frontendConfig, }, }), @@ -167,13 +134,16 @@ export async function createConfig( }), ); + // Detect and use the appropriate react-dom hot-loader patch based on what + // version of React is used within the target repo. const resolveAliases: Record = {}; try { // eslint-disable-next-line import/no-extraneous-dependencies const { version: reactDomVersion } = require('react-dom/package.json'); - // Only apply the alias for hook support if we're running with React 16 if (reactDomVersion.startsWith('16.')) { - resolveAliases['react-dom'] = '@hot-loader/react-dom'; + resolveAliases['react-dom'] = '@hot-loader/react-dom-v16'; + } else { + resolveAliases['react-dom'] = '@hot-loader/react-dom-v17'; } } catch (error) { console.warn(`WARNING: Failed to read react-dom version, ${error}`); diff --git a/packages/cli/src/lib/diff/handlers.ts b/packages/cli/src/lib/diff/handlers.ts index 5bd90e3d22..55e9cddd3d 100644 --- a/packages/cli/src/lib/diff/handlers.ts +++ b/packages/cli/src/lib/diff/handlers.ts @@ -173,6 +173,14 @@ class PackageJsonHandler { return; } + // Skip diffing package scripts that have been migrated to the new commands + const hasNewScript = Object.values(targetScripts).some(script => + String(script).includes('backstage-cli package '), + ); + if (hasNewScript) { + return; + } + for (const key of Object.keys(pkgScripts)) { await this.syncField(key, pkgScripts, targetScripts, 'scripts'); } diff --git a/packages/cli/src/lib/git.test.ts b/packages/cli/src/lib/git.test.ts new file mode 100644 index 0000000000..2e5d464796 --- /dev/null +++ b/packages/cli/src/lib/git.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { runGit, listChangedFiles } from './git'; + +describe('runGit', () => { + it('runs a git command', async () => { + await expect(runGit('log', 'HEAD..HEAD')).resolves.toEqual(['']); + }); + + it('fails for unknown commands', async () => { + await expect(runGit('ryckbegäran')).rejects.toThrow( + /^git ryckbegäran failed, git: 'ryckbegäran' is not a git command/, + ); + }); + + it('forwards failures', async () => { + await expect( + runGit( + 'show', + '--quiet', + '--pretty=format:%s', + '0000000000000000000000000000000000000000', + ), + ).rejects.toThrow( + 'git show failed, fatal: bad object 0000000000000000000000000000000000000000', + ); + }); +}); + +describe('listChangedFiles', () => { + it('requires a ref', async () => { + await expect(listChangedFiles('')).rejects.toThrow('ref is required'); + }); + + it('should return something', async () => { + await expect(listChangedFiles('HEAD')).resolves.toEqual(expect.any(Array)); + }); +}); diff --git a/packages/cli/src/lib/git.ts b/packages/cli/src/lib/git.ts new file mode 100644 index 0000000000..f8783804d1 --- /dev/null +++ b/packages/cli/src/lib/git.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assertError, ForwardedError } from '@backstage/errors'; +import { execFile as execFileCb } from 'child_process'; +import { promisify } from 'util'; +import { paths } from './paths'; + +const execFile = promisify(execFileCb); + +/** + * Run a git command, trimming the output splitting it into lines. + */ +export async function runGit(...args: string[]) { + try { + const { stdout } = await execFile('git', args, { + shell: true, + cwd: paths.targetRoot, + }); + return stdout.trim().split(/\r\n|\r|\n/); + } catch (error) { + assertError(error); + if (error.stderr || typeof error.code === 'number') { + const stderr = (error.stderr as undefined | Buffer)?.toString('utf8'); + const msg = stderr?.trim() ?? `with exit code ${error.code}`; + throw new Error(`git ${args[0]} failed, ${msg}`); + } + throw new ForwardedError('Unknown execution error', error); + } +} + +/** + * Returns a sorted list of all files that have changed since the merge base + * of the provided `ref` and HEAD, as well as all files that are not tracked by git. + */ +export async function listChangedFiles(ref: string) { + if (!ref) { + throw new Error('ref is required'); + } + const [base] = await runGit('merge-base', 'HEAD', ref); + + const tracked = await runGit('diff', '--name-only', base); + const untracked = await runGit('ls-files', '--others', '--exclude-standard'); + + return Array.from(new Set([...tracked, ...untracked])); +} diff --git a/packages/cli/src/lib/monorepo/PackageGraph.test.ts b/packages/cli/src/lib/monorepo/PackageGraph.test.ts index e8e531bc93..06427a7313 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.test.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.test.ts @@ -15,8 +15,20 @@ */ import { getPackages } from '@manypkg/get-packages'; -import { paths } from '../paths'; import { PackageGraph } from './PackageGraph'; +import { listChangedFiles } from '../git'; + +jest.mock('../git'); + +const mockListChangedFiles = listChangedFiles as jest.MockedFunction< + typeof listChangedFiles +>; + +jest.mock('../paths', () => ({ + paths: { + targetRoot: '/', + }, +})); const testPackages = [ { @@ -53,7 +65,7 @@ const testPackages = [ describe('PackageGraph', () => { it('is able to construct a graph from this repo', async () => { - const { packages } = await getPackages(paths.ownDir); + const { packages } = await getPackages(__dirname); const graph = PackageGraph.fromPackages(packages); expect(graph.has('@backstage/cli')).toBe(true); }); @@ -124,4 +136,24 @@ describe('PackageGraph', () => { `Package 'unknown' not found`, ); }); + + it('lists changed packages', async () => { + const graph = PackageGraph.fromPackages(testPackages); + + mockListChangedFiles.mockResolvedValueOnce( + [ + 'README.md', + 'packages/a/src/foo.ts', + 'packages/a/src/bar.ts', + 'packages/b/package.json', + 'packages/f/src/foo.ts', + 'packages/f/README.md', + 'plugins/foo/src/index.ts', + ].sort(), + ); + + await expect( + graph.listChangedPackages({ ref: 'origin/master' }), + ).resolves.toEqual([graph.get('a'), graph.get('b')]); + }); }); diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index 89d5f79cd2..4bbb9eac67 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -14,9 +14,11 @@ * limitations under the License. */ +import path from 'path'; import { getPackages, Package } from '@manypkg/get-packages'; import { paths } from '../paths'; import { PackageRole } from '../role'; +import { listChangedFiles } from '../git'; type PackageJSON = Package['packageJson']; @@ -158,4 +160,50 @@ export class PackageGraph extends Map { return targets; } + + async listChangedPackages(options: { ref: string }) { + const changedFiles = await listChangedFiles(options.ref); + + const dirMap = new Map( + Array.from(this.values()).map(pkg => [ + // relative from root, convert to posix, and add a / at the end + path + .relative(paths.targetRoot, pkg.dir) + .split(path.sep) + .join(path.posix.sep) + path.posix.sep, + pkg, + ]), + ); + const packageDirs = Array.from(dirMap.keys()); + + const result = new Array(); + let searchIndex = 0; + + changedFiles.sort(); + packageDirs.sort(); + + for (const packageDir of packageDirs) { + // Skip through changes that appear before our package dir + while ( + searchIndex < changedFiles.length && + changedFiles[searchIndex] < packageDir + ) { + searchIndex += 1; + } + + // Check if we arrived at a match, otherwise we move on to the next package dir + if (changedFiles[searchIndex]?.startsWith(packageDir)) { + searchIndex += 1; + + result.push(dirMap.get(packageDir)!); + + // Skip through the rest of the changed files for the same package + while (changedFiles[searchIndex]?.startsWith(packageDir)) { + searchIndex += 1; + } + } + } + + return result; + } } diff --git a/packages/cli/src/lib/monorepo/index.ts b/packages/cli/src/lib/monorepo/index.ts index aa12c63abc..7156975c07 100644 --- a/packages/cli/src/lib/monorepo/index.ts +++ b/packages/cli/src/lib/monorepo/index.ts @@ -15,4 +15,8 @@ */ export { PackageGraph } from './PackageGraph'; -export type { PackageGraphNode } from './PackageGraph'; +export type { + PackageGraphNode, + ExtendedPackage, + ExtendedPackageJSON, +} from './PackageGraph'; diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/lib/packager/createDistWorkspace.ts index 15ccda30c2..bce6c5c14c 100644 --- a/packages/cli/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/lib/packager/createDistWorkspace.ts @@ -38,6 +38,7 @@ import { Output, } from '../builder'; import { copyPackageDist } from './copyPackageDist'; +import { getRoleInfo } from '../role'; // These packages aren't safe to pack in parallel since the CLI depends on them const UNSAFE_PACKAGES = [ @@ -131,7 +132,9 @@ export async function createDistWorkspace( } const role = pkg.packageJson.backstage?.role; if (!role) { - console.warn(`Ignored ${pkg.packageJson.name} because it has no role`); + console.warn( + `Building ${pkg.packageJson.name} separately because it has no role`, + ); customBuild.push(pkg.packageJson.name); continue; } @@ -142,9 +145,17 @@ export async function createDistWorkspace( continue; } - if (!buildScript.startsWith('backstage-cli script build')) { + if (!buildScript.startsWith('backstage-cli package build')) { console.warn( - `Ignored ${pkg.packageJson.name} because it has a custom build script, '${buildScript}'`, + `Building ${pkg.packageJson.name} separately because it has a custom build script, '${buildScript}'`, + ); + customBuild.push(pkg.packageJson.name); + continue; + } + + if (getRoleInfo(role).output.includes('bundle')) { + console.warn( + `Building ${pkg.packageJson.name} separately because it is a bundled package`, ); customBuild.push(pkg.packageJson.name); continue; @@ -216,7 +227,7 @@ export async function createDistWorkspace( const FAST_PACK_SCRIPTS = [ undefined, 'backstage-cli prepack', - 'backstage-cli script prepack', + 'backstage-cli package prepack', ]; async function moveToDistWorkspace( diff --git a/packages/cli/src/lib/parallel.test.ts b/packages/cli/src/lib/parallel.test.ts index 824a83d335..032d66afac 100644 --- a/packages/cli/src/lib/parallel.test.ts +++ b/packages/cli/src/lib/parallel.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import os from 'os'; import { parseParallelismOption, getEnvironmentParallelism, @@ -22,6 +23,8 @@ import { runWorkerThreads, } from './parallel'; +const defaultParallelism = Math.ceil(os.cpus().length / 2); + describe('parseParallelismOption', () => { it('coerces false no parallelism', () => { expect(parseParallelismOption(false)).toBe(1); @@ -29,10 +32,10 @@ describe('parseParallelismOption', () => { }); 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); + expect(parseParallelismOption(true)).toBe(defaultParallelism); + expect(parseParallelismOption('true')).toBe(defaultParallelism); + expect(parseParallelismOption(undefined)).toBe(defaultParallelism); + expect(parseParallelismOption(null)).toBe(defaultParallelism); }); it('coerces number string to number', () => { @@ -52,13 +55,13 @@ describe('getEnvironmentParallelism', () => { expect(getEnvironmentParallelism()).toBe(2); process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'true'; - expect(getEnvironmentParallelism()).toBe(4); + expect(getEnvironmentParallelism()).toBe(defaultParallelism); process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'false'; expect(getEnvironmentParallelism()).toBe(1); delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; - expect(getEnvironmentParallelism()).toBe(4); + expect(getEnvironmentParallelism()).toBe(defaultParallelism); }); }); @@ -70,6 +73,7 @@ describe('runParallelWorkers', () => { const work = runParallelWorkers({ items: [0, 1, 2, 3, 4], + parallelismSetting: 4, parallelismFactor: 0.5, // 2 at a time worker: async item => { started.push(item); @@ -197,15 +201,15 @@ describe('runWorkerThreads', () => { await runWorkerThreads({ threadCount: 2, worker: async (_data, sendMessage) => { - sendMessage('foo'); - await new Promise(resolve => setTimeout(resolve, 50)); - sendMessage('bar'); - await new Promise(resolve => setTimeout(resolve, 50)); - sendMessage('baz'); + 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).toEqual(['foo', 'foo', 'bar', 'bar', 'baz', 'baz']); + 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 246020fcc3..7b4ae4609e 100644 --- a/packages/cli/src/lib/parallel.ts +++ b/packages/cli/src/lib/parallel.ts @@ -14,20 +14,21 @@ * limitations under the License. */ +import os from 'os'; import { ErrorLike } from '@backstage/errors'; import { Worker } from 'worker_threads'; -export const DEFAULT_PARALLELISM = 4; +const defaultParallelism = Math.ceil(os.cpus().length / 2); -export const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; +const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; export type ParallelismOption = boolean | string | number | null | undefined; export function parseParallelismOption(parallel: ParallelismOption): number { if (parallel === undefined || parallel === null) { - return DEFAULT_PARALLELISM; + return defaultParallelism; } else if (typeof parallel === 'boolean') { - return parallel ? DEFAULT_PARALLELISM : 1; + return parallel ? defaultParallelism : 1; } else if (typeof parallel === 'number' && Number.isInteger(parallel)) { if (parallel < 1) { return 1; @@ -134,10 +135,14 @@ export type WorkerQueueThreadsOptions = { * 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; + workerFactory: ( + data: TData, + ) => + | ((item: TItem) => Promise) + | Promise<(item: TItem) => Promise>; /** Data supplied to each worker factory */ workerData?: TData; - /** Number of threads, defaults to the environment parallelism */ + /** Number of threads, defaults to half of the number of available CPUs */ threadCount?: number; }; @@ -148,13 +153,14 @@ export type WorkerQueueThreadsOptions = { export async function runWorkerQueueThreads( options: WorkerQueueThreadsOptions, ): Promise { + const items = Array.from(options.items); const { workerFactory, workerData, - threadCount = getEnvironmentParallelism(), + threadCount = Math.min(getEnvironmentParallelism(), items.length), } = options; - const iterator = options.items[Symbol.iterator](); + const iterator = items[Symbol.iterator](); const results = new Array(); let itemIndex = 0; @@ -208,31 +214,39 @@ export async function runWorkerQueueThreads( } function workerQueueThread( - workerFuncFactory: (data: unknown) => (item: unknown) => Promise, + workerFuncFactory: ( + data: unknown, + ) => Promise<(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, + Promise.resolve() + .then(() => workerFuncFactory(workerData)) + .then( + workerFunc => { + 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 }); + } + } }); - } catch (error) { - parentPort.postMessage({ type: 'error', error }); - } - } - }); - parentPort.postMessage({ type: 'start' }); + parentPort.postMessage({ type: 'start' }); + }, + error => parentPort.postMessage({ type: 'error', error }), + ); } export type WorkerThreadsOptions = { diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index f6c9fb418a..759be462d2 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -31,8 +31,8 @@ describe('getRoleInfo', () => { output: ['types', 'esm'], }); - expect(getRoleInfo('app')).toEqual({ - role: 'app', + expect(getRoleInfo('frontend')).toEqual({ + role: 'frontend', platform: 'web', output: ['bundle'], }); @@ -56,10 +56,10 @@ describe('getRoleFromPackage', () => { expect( getRoleFromPackage({ backstage: { - role: 'app', + role: 'frontend', }, }), - ).toEqual('app'); + ).toEqual('frontend'); expect(() => getRoleFromPackage({ @@ -135,7 +135,7 @@ describe('detectRoleFromPackage', () => { 'cy:run': 'cypress run', }, }), - ).toEqual('app'); + ).toEqual('frontend'); }); it('detects the role of example-backend', () => { @@ -180,7 +180,7 @@ describe('detectRoleFromPackage', () => { clean: 'backstage-cli clean', }, }), - ).toEqual('plugin-frontend'); + ).toEqual('frontend-plugin'); }); it('detects the role of @backstage/plugin-catalog-backend', () => { @@ -204,7 +204,7 @@ describe('detectRoleFromPackage', () => { clean: 'backstage-cli clean', }, }), - ).toEqual('plugin-backend'); + ).toEqual('backend-plugin'); }); it('detects the role of @backstage/plugin-catalog-react', () => { @@ -274,7 +274,7 @@ describe('detectRoleFromPackage', () => { clean: 'backstage-cli clean', }, }), - ).toEqual('plugin-backend-module'); + ).toEqual('backend-plugin-module'); }); it('detects the role of @backstage/plugin-permission-node', () => { @@ -323,6 +323,6 @@ describe('detectRoleFromPackage', () => { clean: 'backstage-cli clean', }, }), - ).toEqual('plugin-frontend-module'); + ).toEqual('frontend-plugin-module'); }); }); diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index 7c855ee0fd..a33a7d715a 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -22,7 +22,7 @@ import { PackageRole, PackageRoleInfo } from './types'; const packageRoleInfos: PackageRoleInfo[] = [ { - role: 'app', + role: 'frontend', platform: 'web', output: ['bundle'], }, @@ -52,22 +52,22 @@ const packageRoleInfos: PackageRoleInfo[] = [ output: ['types', 'esm', 'cjs'], }, { - role: 'plugin-frontend', + role: 'frontend-plugin', platform: 'web', output: ['types', 'esm'], }, { - role: 'plugin-frontend-module', + role: 'frontend-plugin-module', platform: 'web', output: ['types', 'esm'], }, { - role: 'plugin-backend', + role: 'backend-plugin', platform: 'node', output: ['types', 'cjs'], }, { - role: 'plugin-backend-module', + role: 'backend-plugin-module', platform: 'node', output: ['types', 'cjs'], }, @@ -147,22 +147,22 @@ export function detectRoleFromPackage( const pkg = detectionSchema.parse(pkgJson); if (pkg.scripts?.start?.includes('app:serve')) { - return 'app'; + return 'frontend'; } if (pkg.scripts?.build?.includes('backend:bundle')) { return 'backend'; } if (pkg.name?.includes('plugin-') && pkg.name?.includes('-backend-module-')) { - return 'plugin-backend-module'; + return 'backend-plugin-module'; } if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) { - return 'plugin-frontend-module'; + return 'frontend-plugin-module'; } if (pkg.scripts?.start?.includes('plugin:serve')) { - return 'plugin-frontend'; + return 'frontend-plugin'; } if (pkg.scripts?.start?.includes('backend:dev')) { - return 'plugin-backend'; + return 'backend-plugin'; } const mainEntry = pkg.publishConfig?.main || pkg.main; diff --git a/packages/cli/src/lib/role/types.ts b/packages/cli/src/lib/role/types.ts index 6a89f7bdaf..a3961544f4 100644 --- a/packages/cli/src/lib/role/types.ts +++ b/packages/cli/src/lib/role/types.ts @@ -15,16 +15,16 @@ */ export type PackageRole = - | 'app' + | 'frontend' | 'backend' | 'cli' | 'web-library' | 'node-library' | 'common-library' - | 'plugin-frontend' - | 'plugin-frontend-module' - | 'plugin-backend' - | 'plugin-backend-module'; + | 'frontend-plugin' + | 'frontend-plugin-module' + | 'backend-plugin' + | 'backend-plugin-module'; export type PackagePlatform = 'node' | 'web' | 'common'; export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs'; diff --git a/packages/cli/src/lib/svgrTemplate.ts b/packages/cli/src/lib/svgrTemplate.ts index ca2a8c38fa..6af1b95e71 100644 --- a/packages/cli/src/lib/svgrTemplate.ts +++ b/packages/cli/src/lib/svgrTemplate.ts @@ -20,28 +20,23 @@ * them with .icon.svg */ export function svgrTemplate( - { template }: any, - _opts: any, - { imports, interfaces, componentName, jsx }: any, + { imports, interfaces, componentName, props, jsx }: any, + { tpl }: any, ) { - const iconName = { - ...componentName, - name: `${componentName.name.replace(/icon$/, '')}Icon`, - }; + const name = `${componentName.replace(/icon$/, '')}Icon`; const defaultExport = { type: 'ExportDefaultDeclaration', - declaration: iconName, + declaration: { type: 'Identifier', name }, }; - const typeScriptTemplate = template.smart({ plugins: ['typescript'] }); - return typeScriptTemplate.ast` + return tpl` ${imports} import SvgIcon from '@material-ui/core/SvgIcon'; ${interfaces} -const ${iconName} = props => React.createElement(SvgIcon, props, ${jsx.children}); +const ${name} = (${props}) => React.createElement(SvgIcon, ${props}, ${jsx.children}); ${defaultExport}`; } diff --git a/packages/codemods/.snyk b/packages/codemods/.snyk new file mode 100644 index 0000000000..7926925d71 --- /dev/null +++ b/packages/codemods/.snyk @@ -0,0 +1,12 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-UNSETVALUE-2400660: + - '*': + reason: >- + The pollution in this vulnerability does not allow injection of + arbitrary values and is therefore a negligible risk for a CLI. + expires: 2022-08-17T21:54:43.658Z + created: 2022-02-15T21:54:43.668Z +patch: {} diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index c7283f5d8c..603fa1075e 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/codemods +## 0.1.35 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.8 + +## 0.1.34 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-app-api@0.5.3 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/cli-common@0.1.7 + ## 0.1.33 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index dc1fe6a537..c4d2eedb4d 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,12 +1,15 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.33", + "version": "0.1.34", "private": false, "publishConfig": { "access": "public", "main": "dist/index.cjs.js" }, + "backstage": { + "role": "cli" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -20,18 +23,18 @@ "main": "src/index.ts", "scripts": { "start": "nodemon --", - "build": "backstage-cli build --outputs cjs", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "clean": "backstage-cli package clean" }, "bin": { "backstage-codemods": "bin/backstage-codemods" }, "dependencies": { - "@backstage/cli-common": "0.1.6", + "@backstage/cli-common": "^0.1.8", "@backstage/core-app-api": "*", "@backstage/core-components": "*", "@backstage/core-plugin-api": "*", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index cf953948da..d9bd7be8ba 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/config-loader +## 0.9.6 + +### Patch Changes + +- c3a1300f79: Include any files included in configuration via $include or $file directives when watching for configuration changes. + +## 0.9.5 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/cli-common@0.1.8 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/types@0.1.3 + +## 0.9.4 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/errors@0.2.1 + - @backstage/cli-common@0.1.7 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.9.3 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index b374a02fbf..d9c9b551b4 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.9.3", + "version": "0.9.6", "private": false, "publishConfig": { "access": "public", @@ -9,6 +9,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "node-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,18 +25,19 @@ "main": "src/index.ts", "types": "src/index.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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/cli-common": "^0.1.6", - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", - "@backstage/types": "^0.1.1", + "@backstage/cli-common": "^0.1.8", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/types": "^0.1.3", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", "chokidar": "^3.5.2", diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index a05796139b..3ff161cec4 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -182,7 +182,7 @@ function compileTsSchemas(paths: string[]) { program, // All schemas should export a `Config` symbol 'Config', - // This enables usage of @visibility and @deprecated in doc comments + // This enables the use of these tags in TSDoc comments { required: true, validationKeywords: ['visibility', 'deprecated'], diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index 9faeda64ea..058f59aef1 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -333,6 +333,66 @@ describe('loadConfig', () => { stopSignal.resolve(); }); + it('watches included files', async () => { + const onChange = defer(); + const stopSignal = defer(); + + await expect( + loadConfig({ + configRoot: '/root', + configTargets: [{ path: '/root/app-config.development.yaml' }], + watch: { + onChange: onChange.resolve, + stopSignal: stopSignal.promise, + }, + }), + ).resolves.toEqual({ + appConfigs: [ + { + context: 'app-config.development.yaml', + data: { + app: { + sessionKey: 'development-key', + }, + backend: { + foo: { + bar: 'token is-secret', + }, + }, + other: { + secret: 'abc123', + }, + }, + }, + ], + }); + + // session-key is indirectly included in app-config.development.yaml + // via included.yaml + await fs.writeFile('/root/secrets/session-key.txt', 'abc234'); + + await expect(onChange.promise).resolves.toEqual([ + { + context: 'app-config.development.yaml', + data: { + app: { + sessionKey: 'development-key', + }, + backend: { + foo: { + bar: 'token is-secret', + }, + }, + other: { + secret: 'abc234', + }, + }, + }, + ]); + + stopSignal.resolve(); + }); + it('watches remote config urls', async () => { server.use(initialLoaderHandler); diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 03a6805da0..9a3287e285 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -140,7 +140,8 @@ export async function loadConfig( const env = envFunc ?? (async (name: string) => process.env[name]); const loadConfigFiles = async () => { - const configs = []; + const fileConfigs = []; + const loadedPaths = new Set(); for (const configPath of configPaths) { if (!isAbsolute(configPath)) { @@ -148,8 +149,15 @@ export async function loadConfig( } const dir = dirname(configPath); - const readFile = (path: string) => - fs.readFile(resolvePath(dir, path), 'utf8'); + const readFile = (path: string) => { + const fullPath = resolvePath(dir, path); + // if we read a file when building configuration, + // we should include that file when watching for + // changes, too. + loadedPaths.add(fullPath); + + return fs.readFile(fullPath, 'utf8'); + }; const input = yaml.parse(await readFile(configPath)); const substitutionTransform = createSubstitutionTransform(env); @@ -158,10 +166,10 @@ export async function loadConfig( substitutionTransform, ]); - configs.push({ data, context: basename(configPath) }); + fileConfigs.push({ data, context: basename(configPath) }); } - return configs; + return { fileConfigs, loadedPaths }; }; const loadRemoteConfigFiles = async () => { @@ -199,8 +207,9 @@ export async function loadConfig( }; let fileConfigs: AppConfig[]; + let loadedPaths: Set; try { - fileConfigs = await loadConfigFiles(); + ({ fileConfigs, loadedPaths } = await loadConfigFiles()); } catch (error) { throw new ForwardedError('Failed to read static configuration file', error); } @@ -220,14 +229,24 @@ export async function loadConfig( const envConfigs = await readEnvConfig(process.env); const watchConfigFile = (watchProp: LoadConfigOptionsWatch) => { - const watcher = chokidar.watch(configPaths, { + let watchedFiles = Array.from(loadedPaths); + + const watcher = chokidar.watch(watchedFiles, { usePolling: process.env.NODE_ENV === 'test', }); let currentSerializedConfig = JSON.stringify(fileConfigs); watcher.on('change', async () => { try { - const newConfigs = await loadConfigFiles(); + const { fileConfigs: newConfigs, loadedPaths: newLoadedPaths } = + await loadConfigFiles(); + + // Replace watches to handle any added or removed + // $include or $file expressions. + watcher.unwatch(watchedFiles); + watchedFiles = Array.from(newLoadedPaths); + watcher.add(watchedFiles); + const newSerializedConfig = JSON.stringify(newConfigs); if (currentSerializedConfig === newSerializedConfig) { diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 2d3e90d1b7..2af4d3763b 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/config +## 0.1.15 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/types@0.1.3 + +## 0.1.14 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/types@0.1.2 + ## 0.1.13 ### Patch Changes diff --git a/packages/config/package.json b/packages/config/package.json index 02c63af2b5..63edfe0360 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.13", + "version": "0.1.15", "private": false, "publishConfig": { "access": "public", @@ -9,6 +9,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,19 +25,19 @@ "main": "src/index.ts", "types": "src/index.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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/types": "^0.1.1", + "@backstage/types": "^0.1.3", "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/test-utils": "^0.2.4-next.0", + "@backstage/test-utils": "^0.2.5", "@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 cc115a7da2..f40ea23f73 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/core-app-api +## 0.5.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.7.0 + +## 0.5.3 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-plugin-api@0.6.1 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + - @backstage/version-bridge@0.1.2 + ## 0.5.2 ### Patch Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 3b53813615..116b33c598 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -16,7 +16,6 @@ import { AppConfig } from '@backstage/config'; import { AppTheme } from '@backstage/core-plugin-api'; import { AppThemeApi } from '@backstage/core-plugin-api'; import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; -import { auth0AuthApiRef } from '@backstage/core-plugin-api'; import { AuthProviderInfo } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; @@ -246,12 +245,6 @@ export class AtlassianAuth { static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; } -// @public @deprecated -export class Auth0Auth { - // (undocumented) - static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; -} - // @public export type AuthApiCreateOptions = { discoveryApi: DiscoveryApi; @@ -383,21 +376,8 @@ export type FlatRoutesProps = { export class GithubAuth { // (undocumented) static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T; - // @deprecated (undocumented) - static normalizeScope(scope?: string): Set; } -// @public @deprecated -export type GithubSession = { - providerInfo: { - accessToken: string; - scopes: Set; - expiresAt?: Date; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - // @public export class GitlabAuth { // (undocumented) @@ -539,13 +519,6 @@ export class SamlAuth signOut(): Promise; } -// @public @deprecated -export type SamlSession = { - userId: string; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - // @public export type SignInPageProps = { onSignInSuccess(identityApi: IdentityApi): void; diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 04e87b5d0c..6f74bf95d6 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,13 +1,16 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.5.2", + "version": "0.5.4", "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,18 +24,19 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli build --outputs types,esm", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/types": "^0.1.1", - "@backstage/version-bridge": "^0.1.1", + "@backstage/config": "^0.1.15", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/types": "^0.1.3", + "@backstage/version-bridge": "^0.1.2", "@types/prop-types": "^15.7.3", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", @@ -45,8 +49,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts index 76cf01d20a..a64fadeb3e 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts @@ -23,13 +23,13 @@ describe('MockOAuthApi', () => { const authHandler1 = jest.fn().mockImplementation(() => authResult); const requester1 = mock.createAuthRequester({ - provider: { icon: () => null, title: 'Test' }, + provider: { icon: () => null, title: 'Test', id: 'test-provider' }, onAuthRequest: authHandler1, }); const authHandler2 = jest.fn().mockResolvedValue('other'); const requester2 = mock.createAuthRequester({ - provider: { icon: () => null, title: 'Test' }, + provider: { icon: () => null, title: 'Test', id: 'test-provider' }, onAuthRequest: authHandler2, }); @@ -66,13 +66,13 @@ describe('MockOAuthApi', () => { const authHandler1 = jest.fn(); const requester1 = mock.createAuthRequester({ - provider: { icon: () => null, title: 'Test' }, + provider: { icon: () => null, title: 'Test', id: 'test-provider' }, onAuthRequest: authHandler1, }); const authHandler2 = jest.fn(); const requester2 = mock.createAuthRequester({ - provider: { icon: () => null, title: 'Test' }, + provider: { icon: () => null, title: 'Test', id: 'test-provider' }, onAuthRequest: authHandler2, }); diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts index 3110197ad9..4dd6af715e 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts @@ -25,6 +25,7 @@ describe('OAuthRequestManager', () => { const requester = manager.createAuthRequester({ provider: { + id: 'my-provider', title: 'My Provider', icon: () => null, }, diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts index f9d9270b2a..5d467be7b4 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts @@ -39,12 +39,6 @@ export class OAuthRequestManager implements OAuthRequestApi { private handlerCount = 0; createAuthRequester(options: OAuthRequesterOptions): OAuthRequester { - if (!options.provider.id) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: Not passing a provider id to createAuthRequester is deprecated, it will be required in the future', - ); - } const handler = new OAuthPendingRequests(); const index = this.handlerCount; diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts index d81cba48d2..2f82ed5e88 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -33,12 +33,11 @@ describe('WebStorage Storage API', () => { it('should return undefined for values which are unset', async () => { const storage = createWebStorage(); - expect(storage.get('myfakekey')).toBeUndefined(); + expect(storage.snapshot('myfakekey').value).toBeUndefined(); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'absent', value: undefined, - newValue: undefined, }); }); @@ -48,26 +47,23 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', 'helloimastring'); await storage.set('mysecondfakekey', 1234); await storage.set('mythirdfakekey', true); - expect(storage.get('myfakekey')).toBe('helloimastring'); - expect(storage.get('mysecondfakekey')).toBe(1234); - expect(storage.get('mythirdfakekey')).toBe(true); + expect(storage.snapshot('myfakekey').value).toBe('helloimastring'); + expect(storage.snapshot('mysecondfakekey').value).toBe(1234); + expect(storage.snapshot('mythirdfakekey').value).toBe(true); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: 'helloimastring', - newValue: 'helloimastring', }); expect(storage.snapshot('mysecondfakekey')).toEqual({ key: 'mysecondfakekey', presence: 'present', value: 1234, - newValue: 1234, }); expect(storage.snapshot('mythirdfakekey')).toEqual({ key: 'mythirdfakekey', presence: 'present', value: true, - newValue: true, }); }); @@ -81,12 +77,11 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', mockData); - expect(storage.get('myfakekey')).toEqual(mockData); + expect(storage.snapshot('myfakekey').value).toEqual(mockData); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -118,7 +113,6 @@ describe('WebStorage Storage API', () => { key: 'correctKey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -152,7 +146,6 @@ describe('WebStorage Storage API', () => { key: 'correctKey', presence: 'absent', value: undefined, - newValue: undefined, }); }); @@ -166,9 +159,11 @@ describe('WebStorage Storage API', () => { await firstStorage.set(keyName, 'boop'); await secondStorage.set(keyName, 'deerp'); - expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); - expect(firstStorage.get(keyName)).toBe('boop'); - expect(secondStorage.get(keyName)).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toBe( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName).value).toBe('boop'); + expect(secondStorage.snapshot(keyName).value).toBe('deerp'); expect(firstStorage.snapshot(keyName)).not.toEqual( secondStorage.snapshot(keyName), ); @@ -176,13 +171,11 @@ describe('WebStorage Storage API', () => { key: keyName, presence: 'present', value: 'boop', - newValue: 'boop', }); expect(secondStorage.snapshot(keyName)).toEqual({ key: keyName, presence: 'present', value: 'deerp', - newValue: 'deerp', }); }); @@ -217,7 +210,6 @@ describe('WebStorage Storage API', () => { key: 'key', presence: 'absent', value: undefined, - newValue: undefined, }); expect(mockErrorApi.post).toHaveBeenCalledWith(expect.any(Error)); expect(mockErrorApi.post).toHaveBeenCalledWith( diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts index c4cffaf184..ac3d3f20f2 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -65,7 +65,7 @@ export class WebStorage implements StorageApi { new Error(`Error when parsing JSON config from storage for: ${key}`), ); } - return { key, value, newValue: value, presence }; + return { key, value, presence }; } forBucket(name: string): WebStorage { diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts deleted file mode 100644 index d8f942b94b..0000000000 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ /dev/null @@ -1,67 +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 { auth0AuthApiRef } from '@backstage/core-plugin-api'; -import { OAuth2 } from '../oauth2'; -import { OAuthApiCreateOptions } from '../types'; - -const DEFAULT_PROVIDER = { - id: 'auth0', - title: 'Auth0', - icon: () => null, -}; - -/** - * Implements the OAuth flow to Auth0 products. - * - * @public - * @deprecated Use {@link OAuth2} instead - * - * @example - * - * ```ts - * OAuth2.create({ - * discoveryApi, - * oauthRequestApi, - * provider: { - * id: 'auth0', - * title: 'Auth0', - * icon: () => null, - * }, - * defaultScopes: ['openid', 'email', 'profile'], - * environment: configApi.getOptionalString('auth.environment'), - * }) - * ``` - */ -export default class Auth0Auth { - static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T { - const { - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['openid', `email`, `profile`], - } = options; - - return OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes, - }); - } -} 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 b0af4c7ade..7efe4e95c6 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 @@ -47,19 +47,4 @@ export default class GithubAuth { defaultScopes, }); } - - /** - * @deprecated This method is deprecated and will be removed in a future release. - */ - static normalizeScope(scope?: string): Set { - if (!scope) { - return new Set(); - } - - const scopeList = Array.isArray(scope) - ? scope - : scope.split(/[\s|,]/).filter(Boolean); - - return new Set(scopeList); - } } diff --git a/packages/core-app-api/src/apis/implementations/auth/github/index.ts b/packages/core-app-api/src/apis/implementations/auth/github/index.ts index b5aa1a0a25..5e53bacfc1 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export type { GithubSession } from './types'; export { default as GithubAuth } from './GithubAuth'; diff --git a/packages/core-app-api/src/apis/implementations/auth/github/types.ts b/packages/core-app-api/src/apis/implementations/auth/github/types.ts deleted file mode 100644 index 0ef662905f..0000000000 --- a/packages/core-app-api/src/apis/implementations/auth/github/types.ts +++ /dev/null @@ -1,62 +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 { - ProfileInfo, - BackstageIdentityResponse, -} from '@backstage/core-plugin-api'; -import { z } from 'zod'; - -// TODO(Rugvip): Make GithubSession internal - -/** - * Session information for GitHub auth. - * - * @public - * @deprecated This type is internal and will be removed - */ -export type GithubSession = { - providerInfo: { - accessToken: string; - scopes: Set; - expiresAt?: Date; - }; - profile: ProfileInfo; - // TODO(Rugvip): This should be made optional once the type is no longer public - backstageIdentity: BackstageIdentityResponse; -}; - -export const githubSessionSchema: z.ZodSchema = z.object({ - providerInfo: z.object({ - accessToken: z.string(), - scopes: z.set(z.string()), - expiresAt: z.date().optional(), - }), - profile: z.object({ - email: z.string().optional(), - displayName: z.string().optional(), - picture: z.string().optional(), - }), - backstageIdentity: z.object({ - id: z.string(), - token: z.string(), - identity: z.object({ - type: z.literal('user'), - userEntityRef: z.string(), - ownershipEntityRefs: z.array(z.string()), - }), - }), -}); diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index 50333f07a0..c4cf520db5 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -20,7 +20,6 @@ export * from './google'; export * from './oauth2'; export * from './okta'; export * from './saml'; -export * from './auth0'; export * from './microsoft'; export * from './onelogin'; export * from './bitbucket'; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/index.ts b/packages/core-app-api/src/apis/implementations/auth/saml/index.ts index 2e749a0648..930e6cb115 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/index.ts @@ -14,4 +14,3 @@ * limitations under the License. */ export { default as SamlAuth } from './SamlAuth'; -export type { ExportedSamlSession as SamlSession } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts index f1345fd154..55b278b429 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts @@ -20,18 +20,6 @@ import { } from '@backstage/core-plugin-api'; import { z } from 'zod'; -/** - * Session information for SAML auth. - * - * @public - * @deprecated This type is internal and will be removed - */ -export type ExportedSamlSession = { - userId: string; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - /** @internal */ export type SamlSession = { profile: ProfileInfo; diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index fe51935ab0..5d8211d303 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/core-components +## 0.8.10 + +### Patch Changes + +- d91d22bb19: When clicking on a log line the URL will be updated from `/task/uid` to + `/task/uid/#line-1`. This URL are also sharable, meaning that the UI will + highlight the log line in the hash of the URL. +- Updated dependencies + - @backstage/core-plugin-api@0.7.0 + +## 0.8.9 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 126074a04b: Port supported react-use functions to react-hookz. +- Updated dependencies + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + ## 0.8.8 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index ecb910c50b..87d9f78fcc 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,13 +1,16 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.8.8", + "version": "0.8.10", "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,18 +24,19 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli build --outputs types,esm", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/theme": "^0.2.14", + "@backstage/config": "^0.1.15", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/theme": "^0.2.15", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -59,8 +63,9 @@ "react-router-dom": "6.0.0-beta.0", "react-sparklines": "^1.7.0", "react-syntax-highlighter": "^15.4.5", - "react-text-truncate": "^0.17.0", - "react-use": "^17.2.4", + "react-text-truncate": "^0.18.0", + "react-use": "^17.3.2", + "@react-hookz/web": "^12.3.0", "react-virtualized-auto-sizer": "^1.0.6", "react-window": "^1.8.6", "remark-gfm": "^3.0.1", @@ -73,9 +78,9 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/core-app-api": "^0.5.2", - "@backstage/cli": "^0.13.2", - "@backstage/test-utils": "^0.2.4", + "@backstage/core-app-api": "^0.5.4", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx index e62b3fa82d..3348c278ee 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx @@ -75,7 +75,7 @@ describe('', () => { ); fireEvent.click(button); const dismissedBanners = - notifications?.get('dismissedBanners') ?? []; + notifications?.snapshot('dismissedBanners').value ?? []; expect( dismissedBanners.includes('catalog_page_welcome_banner'), ).toBeTruthy(); diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index 566b326cb5..9ff7616c45 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -101,7 +101,7 @@ export const DismissableBanner = (props: Props) => { const storageApi = useApi(storageApiRef); const notificationsStore = storageApi.forBucket('notifications'); const rawDismissedBanners = - notificationsStore.get('dismissedBanners') ?? []; + notificationsStore.snapshot('dismissedBanners').value ?? []; const [dismissedBanners, setDismissedBanners] = useState( new Set(rawDismissedBanners), @@ -112,11 +112,11 @@ export const DismissableBanner = (props: Props) => { ); useEffect(() => { - if (observedItems?.newValue) { - const currentValue = observedItems?.newValue ?? []; + if (observedItems?.value) { + const currentValue = observedItems?.value ?? []; setDismissedBanners(new Set(currentValue)); } - }, [observedItems?.newValue]); + }, [observedItems?.value]); const handleClick = () => { notificationsStore.set('dismissedBanners', [...dismissedBanners, id]); diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index cd30e96c92..ac66d7cdf5 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -15,6 +15,7 @@ */ import React, { useEffect, useMemo, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; import IconButton from '@material-ui/core/IconButton'; import CopyIcon from '@material-ui/icons/FileCopy'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -42,6 +43,7 @@ export function RealLogViewer(props: RealLogViewerProps) { const search = useLogViewerSearch(lines); const selection = useLogViewerSelection(lines); + const location = useLocation(); useEffect(() => { if (search.resultLine !== undefined && listRef.current) { @@ -49,11 +51,18 @@ export function RealLogViewer(props: RealLogViewerProps) { } }, [search.resultLine]); + useEffect(() => { + if (location.hash) { + // #line-6 -> 6 + const line = parseInt(location.hash.replace(/\D/g, ''), 10); + selection.setSelection(line, false); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + const handleSelectLine = ( line: number, event: { shiftKey: boolean; preventDefault: () => void }, ) => { - event.preventDefault(); selection.setSelection(line, event.shiftKey); }; diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx index 7f0069bc44..14f71984f4 100644 --- a/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx +++ b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx @@ -15,7 +15,7 @@ */ import { useMemo, useState } from 'react'; -import useToggle from 'react-use/lib/useToggle'; +import { useToggle } from '@react-hookz/web'; import { AnsiLine } from './AnsiProcessor'; export function applySearchFilter(lines: AnsiLine[], searchText: string) { diff --git a/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx b/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx index 69952d487a..da489725e4 100644 --- a/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx +++ b/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx @@ -18,7 +18,7 @@ import { makeStyles } from '@material-ui/core/styles'; import Tooltip, { TooltipProps } from '@material-ui/core/Tooltip'; import React, { useState } from 'react'; import TextTruncate, { TextTruncateProps } from 'react-text-truncate'; -import useMountedState from 'react-use/lib/useMountedState'; +import { useIsMounted } from '@react-hookz/web'; type Props = { text: TextTruncateProps['text']; @@ -41,7 +41,7 @@ const useStyles = makeStyles( export function OverflowTooltip(props: Props) { const [hover, setHover] = useState(false); - const isMounted = useMountedState(); + const isMounted = useIsMounted(); const classes = useStyles(); const handleToggled = (truncated: boolean) => { diff --git a/packages/core-components/src/hooks/useQueryParamState.ts b/packages/core-components/src/hooks/useQueryParamState.ts index 72ccafd99f..16cd9e18c8 100644 --- a/packages/core-components/src/hooks/useQueryParamState.ts +++ b/packages/core-components/src/hooks/useQueryParamState.ts @@ -18,7 +18,7 @@ import { isEqual } from 'lodash'; import qs from 'qs'; import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import useDebounce from 'react-use/lib/useDebounce'; +import { useDebouncedEffect } from '@react-hookz/web'; function stringify(queryParams: any): string { // Even though these setting don't look nice (e.g. escaped brackets), we should keep @@ -75,7 +75,7 @@ export function useQueryParamState( ); }, [searchParamsString, setQueryParamState, stateName]); - useDebounce( + useDebouncedEffect( () => { const queryString = joinQueryString( searchParamsString, @@ -87,8 +87,8 @@ export function useQueryParamState( setSearchParams(queryString, { replace: true }); } }, - debounceTime, [setSearchParams, queryParamState, searchParamsString, stateName], + debounceTime, ); return [queryParamState, setQueryParamState]; diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx index adcd6ced8f..13a61d259f 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx @@ -20,7 +20,7 @@ import { useApi, } from '@backstage/core-plugin-api'; import React from 'react'; -import useAsync from 'react-use/lib/useAsync'; +import { useAsync, useMountEffect } from '@react-hookz/web'; import { ErrorPanel } from '../../components/ErrorPanel'; import { Progress } from '../../components/Progress'; import { ProxiedSignInIdentity } from './ProxiedSignInIdentity'; @@ -56,7 +56,7 @@ export type ProxiedSignInPageProps = SignInPageProps & { export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => { const discoveryApi = useApi(discoveryApiRef); - const { loading, error } = useAsync(async () => { + const [{ status, error }, { execute }] = useAsync(async () => { const identity = new ProxiedSignInIdentity({ provider: props.provider, discoveryApi, @@ -65,9 +65,11 @@ export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => { await identity.start(); props.onSignInSuccess(identity); - }, []); + }); - if (loading) { + useMountEffect(execute); + + if (status === 'loading') { return ; } else if (error) { return ( diff --git a/packages/core-components/src/layout/Sidebar/Bar.test.tsx b/packages/core-components/src/layout/Sidebar/Bar.test.tsx index 89133997f9..fc42e7a94e 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.test.tsx @@ -46,6 +46,11 @@ async function renderScalableSidebar() { {}} text="Catalog"> + @@ -112,5 +121,22 @@ describe('Sidebar', () => { '/dropdownitemlink', ); }); + + it('Submenu item renders an external link when `to` value is provided', async () => { + userEvent.hover(screen.getByTestId('item-with-submenu')); + expect(screen.getByText('External Link').closest('a')).toHaveAttribute( + 'href', + 'https://backstage.io/', + ); + }); + + it('Dropdown item in submenu renders an external link when `to` value is provided', async () => { + userEvent.hover(screen.getByTestId('item-with-submenu')); + userEvent.click(screen.getByText('Misc')); + expect(screen.getByText('dropdown item 3').closest('a')).toHaveAttribute( + 'href', + 'https://backstage.io/', + ); + }); }); }); diff --git a/packages/core-components/src/layout/Sidebar/Intro.tsx b/packages/core-components/src/layout/Sidebar/Intro.tsx index 7b2c5fa4d7..1fcc54188f 100644 --- a/packages/core-components/src/layout/Sidebar/Intro.tsx +++ b/packages/core-components/src/layout/Sidebar/Intro.tsx @@ -21,7 +21,7 @@ import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import CloseIcon from '@material-ui/icons/Close'; import React, { useContext, useState } from 'react'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { useLocalStorageValue } from '@react-hookz/web'; import { sidebarConfig, SidebarContext, @@ -155,7 +155,7 @@ export function SidebarIntro(_props: {}) { recentlyViewedItemsDismissed: false, }; const [dismissedIntro, setDismissedIntro] = - useLocalStorage(SIDEBAR_INTRO_LOCAL_STORAGE); + useLocalStorageValue(SIDEBAR_INTRO_LOCAL_STORAGE); const { starredItemsDismissed, recentlyViewedItemsDismissed } = dismissedIntro ?? {}; diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index 1ede5f887e..d204728bcf 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -14,15 +14,10 @@ * limitations under the License. */ import React, { useContext, useState } from 'react'; -import { - NavLink, - resolvePath, - useLocation, - useResolvedPath, -} from 'react-router-dom'; +import { resolvePath, useLocation, useResolvedPath } from 'react-router-dom'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; -import Link from '@material-ui/core/Link'; +import { Link } from '../../components/Link'; import { IconComponent } from '@backstage/core-plugin-api'; import classnames from 'classnames'; import { BackstageTheme } from '@backstage/theme'; @@ -163,7 +158,6 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => {
{dropdownItems.map((object, key) => ( { return (
login({ checkExisting: true })); + useMountEffect(() => login({ checkExisting: true })); return showLoginPage ? ( diff --git a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx deleted file mode 100644 index 739c3709a9..0000000000 --- a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx +++ /dev/null @@ -1,95 +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 React from 'react'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import { InfoCard } from '../InfoCard/InfoCard'; -import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; -import { - useApi, - auth0AuthApiRef, - errorApiRef, -} from '@backstage/core-plugin-api'; -import { ForwardedError } from '@backstage/errors'; -import { UserIdentity } from './UserIdentity'; - -const Component: ProviderComponent = ({ onSignInSuccess }) => { - const auth0AuthApi = useApi(auth0AuthApiRef); - const errorApi = useApi(errorApiRef); - - const handleLogin = async () => { - try { - const identityResponse = await auth0AuthApi.getBackstageIdentity({ - instantPopup: true, - }); - if (!identityResponse) { - throw new Error( - 'The Auth0 provider is not configured to support sign-in', - ); - } - - const profile = await auth0AuthApi.getProfile(); - - onSignInSuccess( - UserIdentity.create({ - identity: identityResponse.identity, - authApi: auth0AuthApi, - profile, - }), - ); - } catch (error) { - errorApi.post(new ForwardedError('Auth0 login failed', error)); - } - }; - - return ( - - - Sign In - - } - > - Sign In using Auth0 - - - ); -}; - -const loader: ProviderLoader = async apis => { - const auth0AuthApi = apis.get(auth0AuthApiRef)!; - - const identityResponse = await auth0AuthApi.getBackstageIdentity({ - optional: true, - }); - - if (!identityResponse) { - return undefined; - } - - const profile = await auth0AuthApi.getProfile(); - return UserIdentity.create({ - identity: identityResponse.identity, - authApi: auth0AuthApi, - profile, - }); -}; - -export const auth0Provider: SignInProvider = { Component, loader }; diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 21668577a1..bba39bf2cf 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/core-plugin-api +## 0.7.0 + +### Minor Changes + +- 33cd215b11: **BREAKING**: Removed deprecated `AnyAnalyticsContext` type which is replaced by `AnalyticsContextValue` + +## 0.6.1 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 2714145cf5: Removes unused react-use dependency. +- Updated dependencies + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + - @backstage/version-bridge@0.1.2 + ## 0.6.0 ### Minor Changes diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 5ac07e195c..89361419ba 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -76,11 +76,6 @@ export type AnalyticsTracker = { ) => void; }; -// @public @deprecated -export type AnyAnalyticsContext = { - [param in string]: string | boolean | number | undefined; -}; - // @public export type AnyApiFactory = ApiFactory< unknown, @@ -189,11 +184,6 @@ export function attachComponentData

( data: unknown, ): void; -// @public @deprecated -export const auth0AuthApiRef: ApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; - // @public export type AuthProviderInfo = { id: string; @@ -536,15 +526,6 @@ export const microsoftAuthApiRef: ApiRef< SessionApi >; -// @public @deprecated -export const oauth2ApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; - // @public export type OAuthApi = { getAccessToken( @@ -571,24 +552,13 @@ export type OAuthRequester = ( // @public export type OAuthRequesterOptions = { - provider: Omit & { - id?: string; - }; + provider: AuthProviderInfo; onAuthRequest(scopes: Set): Promise; }; // @public export type OAuthScope = string | string[]; -// @public @deprecated -export const oidcAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; - // @alpha export const oktaAuthApiRef: ApiRef< OAuthApi & @@ -642,9 +612,7 @@ export type PathParams = { // @public export type PendingOAuthRequest = { - provider: Omit & { - id?: string; - }; + provider: AuthProviderInfo; reject(): void; trigger(): Promise; }; @@ -689,11 +657,6 @@ export type RouteRef = { params: ParamKeys; }; -// @public @deprecated -export const samlAuthApiRef: ApiRef< - ProfileInfoApi & BackstageIdentityApi & SessionApi ->; - // @public export type SessionApi = { signIn(): Promise; @@ -715,8 +678,6 @@ export type SignInPageProps = { // @public export interface StorageApi { forBucket(name: string): StorageApi; - // @deprecated - get(key: string): T | undefined; observe$( key: string, ): Observable>; @@ -728,23 +689,17 @@ export interface StorageApi { // @public export const storageApiRef: ApiRef; -// @public @deprecated (undocumented) -export type StorageValueChange = - StorageValueSnapshot; - // @public export type StorageValueSnapshot = | { key: string; presence: 'unknown' | 'absent'; value?: undefined; - newValue?: undefined; } | { key: string; presence: 'present'; value: TValue; - newValue?: TValue; }; // @public diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index c9f110b432..4e9198557b 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,13 +1,16 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.6.0", + "version": "0.7.0", "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,21 +24,21 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli build --outputs types,esm", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/types": "^0.1.1", - "@backstage/version-bridge": "^0.1.1", + "@backstage/config": "^0.1.15", + "@backstage/types": "^0.1.3", + "@backstage/version-bridge": "^0.1.2", "history": "^5.0.0", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", - "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, "peerDependencies": { @@ -43,9 +46,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", - "@backstage/core-app-api": "^0.5.2-next.0", - "@backstage/test-utils": "^0.2.4-next.0", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-plugin-api/src/analytics/index.ts b/packages/core-plugin-api/src/analytics/index.ts index 942df56ef8..651e8e105e 100644 --- a/packages/core-plugin-api/src/analytics/index.ts +++ b/packages/core-plugin-api/src/analytics/index.ts @@ -15,9 +15,5 @@ */ export { AnalyticsContext } from './AnalyticsContext'; -export type { - AnalyticsContextValue, - AnyAnalyticsContext, - CommonAnalyticsContext, -} from './types'; +export type { AnalyticsContextValue, CommonAnalyticsContext } from './types'; export { useAnalytics } from './useAnalytics'; diff --git a/packages/core-plugin-api/src/analytics/types.ts b/packages/core-plugin-api/src/analytics/types.ts index 431e453a61..1a60572082 100644 --- a/packages/core-plugin-api/src/analytics/types.ts +++ b/packages/core-plugin-api/src/analytics/types.ts @@ -36,16 +36,6 @@ export type CommonAnalyticsContext = { extension: string; }; -/** - * Allows arbitrary scalar values as context attributes too. - * - * @public - * @deprecated Will be removed, use `AnalyticsContextValue` instead - */ -export type AnyAnalyticsContext = { - [param in string]: string | boolean | number | undefined; -}; - /** * Analytics context envelope. * diff --git a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts index ccca2c50e3..75bf3a3864 100644 --- a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -27,10 +27,8 @@ import { AuthProviderInfo } from './auth'; export type OAuthRequesterOptions = { /** * Information about the auth provider, which will be forwarded to auth requests. - * - * Not passing in an `id` is deprecated, and it will be required in the future. */ - provider: Omit & { id?: string }; + provider: AuthProviderInfo; /** * Implementation of the auth flow, which will be called synchronously when @@ -71,10 +69,8 @@ export type OAuthRequester = ( export type PendingOAuthRequest = { /** * Information about the auth provider, as given in the AuthRequesterOptions - * - * Not passing in an `id` is deprecated, and it will be required in the future. */ - provider: Omit & { id?: string }; + provider: AuthProviderInfo; /** * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError". diff --git a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts index 4bd49f9b83..1506e5f143 100644 --- a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts @@ -27,24 +27,13 @@ export type StorageValueSnapshot = key: string; presence: 'unknown' | 'absent'; value?: undefined; - /** @deprecated Use `value` instead */ - newValue?: undefined; } | { key: string; presence: 'present'; value: TValue; - /** @deprecated Use `value` instead */ - newValue?: TValue; }; -/** - * @public - * @deprecated Use StorageValueSnapshot instead - */ -export type StorageValueChange = - StorageValueSnapshot; - /** * Provides a key-value persistence API. * @@ -59,14 +48,6 @@ export interface StorageApi { */ forBucket(name: string): StorageApi; - /** - * Get the current value for persistent data, use observe$ to be notified of updates. - * - * @deprecated Use `snapshot` instead. - * @param key - Unique key associated with the data. - */ - get(key: string): T | undefined; - /** * Remove persistent data. * diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index ed5a7a3264..497ce8233b 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -366,23 +366,6 @@ export const gitlabAuthApiRef: ApiRef< id: 'core.auth.gitlab', }); -/** - * Provides authentication towards Auth0 APIs. - * - * @remarks - * - * See {@link https://auth0.com/docs/scopes/current/oidc-scopes} - * for a full list of supported scopes. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const auth0AuthApiRef: ApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.auth0', -}); - /** * Provides authentication towards Microsoft APIs and identities. * @@ -404,50 +387,6 @@ export const microsoftAuthApiRef: ApiRef< id: 'core.auth.microsoft', }); -/** - * Provides authentication for custom identity providers. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const oauth2ApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.oauth2', -}); - -/** - * Provides authentication for custom OpenID Connect identity providers. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const oidcAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.oidc', -}); - -/** - * Provides authentication for SAML-based identity providers. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const samlAuthApiRef: ApiRef< - ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.saml', -}); - /** * Provides authentication towards OneLogin APIs. * diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 886d873740..52dad1bfc9 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,82 @@ # @backstage/create-app +## 0.4.21 + +### Patch Changes + +- a686702dbe: Update the template to reflect the renaming of `CatalogResultListItem` to `CatalogSearchResultListItem` from `@backstage/plugin-catalog`. + + To apply this change to an existing app, make the following change to `packages/app/src/components/search/SearchPage.tsx`: + + ```diff + -import { CatalogResultListItem } from '@backstage/plugin-catalog'; + +import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; + ``` + + ```diff + case 'software-catalog': + return ( + - + ``` + +- f39c1e6036: To reflect the updated `knex` and `@vscode/sqlite3` dependencies introduced with [v0.4.19](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0419), we update our example `Dockerfile`, adding `@vscode/sqlite3` build dependencies to the image. Further on, we updated it to the `node:16-bullseye-slim` base image. + + To apply this update to an existing app, make the following change to `packages/backend/Dockerfile`: + + ```diff + -FROM node:14-buster-slim + +FROM node:16-bullseye-slim + ``` + + and, _only if you are using sqlite3 in your app_: + + ```diff + RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz + + + +# install sqlite3 dependencies + +RUN apt-get update && \ + + apt-get install -y libsqlite3-dev python3 cmake g++ && \ + + rm -rf /var/lib/apt/lists/* && \ + + yarn config set python /usr/bin/python3 + + RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" + ``` + + If you are using a multi-stage Docker build for your app, please refer to the [updated examples](https://github.com/backstage/backstage/blob/master/docs/deployment/docker.md#multi-stage-build) in the documentation. + +## 0.4.20 + +### Patch Changes + +- e725bb812f: 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"> + - + - + - + + + + ``` + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/cli-common@0.1.7 + ## 0.4.19 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 991a241887..a240e95cac 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,11 +1,14 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.19", + "version": "0.4.21", "private": false, "publishConfig": { "access": "public" }, + "backstage": { + "role": "cli" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,16 +24,16 @@ "backstage-create-app": "bin/backstage-create-app" }, "scripts": { - "build": "backstage-cli build --outputs cjs", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "clean": "backstage-cli clean", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", "prepack": "node scripts/prepack.js", "postpack": "node scripts/postpack.js", "start": "nodemon --" }, "dependencies": { - "@backstage/cli-common": "^0.1.6", + "@backstage/cli-common": "^0.1.8", "chalk": "^4.0.0", "commander": "^6.1.0", "fs-extra": "9.1.0", @@ -48,6 +51,7 @@ "ts-node": "^10.0.0" }, "peerDependencies": { + "@backstage/app-defaults": "", "@backstage/backend-common": "", "@backstage/backend-tasks": "", "@backstage/catalog-client": "", @@ -58,30 +62,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/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 9b6518620d..f4ff424926 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 @@ -15,7 +15,6 @@ import { orgPlugin } from '@backstage/plugin-org'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; import { - DefaultTechDocsHome, TechDocsIndexPage, techdocsPlugin, TechDocsReaderPage, @@ -31,7 +30,7 @@ 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'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; const app = createApp({ apis, @@ -65,9 +64,7 @@ const routes = ( > {entityPage} - }> - - + } /> } 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/app/src/components/search/SearchPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx index a88e7250e9..cd4603ecd3 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core'; -import { CatalogResultListItem } from '@backstage/plugin-catalog'; +import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; import { catalogApiRef, CATALOG_FILTER_EXISTS, } from '@backstage/plugin-catalog-react'; -import { DocsResultListItem } from '@backstage/plugin-techdocs'; +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; import { SearchBar, @@ -116,14 +116,14 @@ const SearchPage = () => { switch (type) { case 'software-catalog': return ( - ); case 'techdocs': return ( - diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index 31231a3a4a..dd35d4ddbc 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -9,7 +9,7 @@ # # Once the commands have been run, you can build the image using `yarn build-image` -FROM node:14-buster-slim +FROM node:16-bullseye-slim WORKDIR /app @@ -19,6 +19,12 @@ WORKDIR /app COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz +# install sqlite3 dependencies +RUN apt-get update && \ + apt-get install -y libsqlite3-dev python3 cmake g++ && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" # Then copy the rest of the backend bundle, along with any other files we might want. diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts index a0a1cc3701..c359cb4986 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts @@ -5,8 +5,8 @@ import { LunrSearchEngine, } from '@backstage/plugin-search-backend-node'; import { PluginEnvironment } from '../types'; -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; export default async function createPlugin({ logger, @@ -23,7 +23,7 @@ export default async function createPlugin({ // collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultCatalogCollator.fromConfig(config, { + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery, tokenManager, }), @@ -32,7 +32,7 @@ export default async function createPlugin({ // collator gathers entities from techdocs. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, logger, tokenManager, diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 8061f5fb98..4d2354297e 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/dev-utils +## 0.2.23 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/app-defaults@0.1.9 + - @backstage/core-app-api@0.5.4 + - @backstage/integration-react@0.1.23 + - @backstage/test-utils@0.2.6 + +## 0.2.22 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-app-api@0.5.3 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/integration-react@0.1.22 + - @backstage/test-utils@0.2.5 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/app-defaults@0.1.8 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.2.21 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 9d55d17592..5c29c7dd0f 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,13 +1,16 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.2.21", + "version": "0.2.23", "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,23 +24,24 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli build --outputs types,esm", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@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.21", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/test-utils": "^0.2.4", - "@backstage/theme": "^0.2.14", + "@backstage/app-defaults": "^0.1.9", + "@backstage/core-app-api": "^0.5.4", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.11.0", + "@backstage/integration-react": "^0.1.23", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/test-utils": "^0.2.6", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.10.1", @@ -55,7 +59,7 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@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 bbf55fac17..b17b0c14ce 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -3,6 +3,9 @@ "description": "E2E test for verifying Backstage packages", "version": "0.2.0", "private": true, + "backstage": { + "role": "cli" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -16,16 +19,18 @@ "main": "src/index.ts", "scripts": { "start": "nodemon --", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "test:e2e": "yarn start" + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "test:e2e": "yarn start", + "build": "backstage-cli package build", + "clean": "backstage-cli package clean" }, "bin": { "e2e-test": "bin/e2e-test" }, "devDependencies": { - "@backstage/cli-common": "^0.1.1", - "@backstage/errors": "^0.2.0", + "@backstage/cli-common": "^0.1.8", + "@backstage/errors": "^0.2.2", "@types/fs-extra": "^9.0.1", "@types/node": "^14.14.32", "@types/puppeteer": "^5.4.4", diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md index 110863d347..0c9295532a 100644 --- a/packages/errors/CHANGELOG.md +++ b/packages/errors/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/errors +## 0.2.2 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/types@0.1.3 + +## 0.2.1 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/types@0.1.2 + ## 0.2.0 ### Minor Changes diff --git a/packages/errors/package.json b/packages/errors/package.json index f97adef8e1..b1c90f0371 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/errors", "description": "Common utilities for error handling within Backstage", - "version": "0.2.0", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -12,6 +12,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,20 +25,20 @@ "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/types": "^0.1.1", + "@backstage/types": "^0.1.3", "cross-fetch": "^3.1.5", "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", + "@backstage/cli": "^0.14.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index ec0457dc1e..1b8f22bcfc 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/integration-react +## 0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration@0.7.5 + +## 0.1.22 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- f0e2f7a56a: Updated the `ScmAuth` error message for missing provider configurations to link to `ScmAuthApi` setup documentation. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/integration@0.7.3 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + ## 0.1.21 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 8413442b3d..27e4492e65 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.21", + "version": "0.1.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,21 +11,24 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/integration": "^0.7.2", - "@backstage/theme": "^0.2.14", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/integration": "^0.7.5", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,9 +38,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/integration-react/src/api/ScmAuth.test.ts b/packages/integration-react/src/api/ScmAuth.test.ts index 4ebfef749e..1ad391d8d2 100644 --- a/packages/integration-react/src/api/ScmAuth.test.ts +++ b/packages/integration-react/src/api/ScmAuth.test.ts @@ -238,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( @@ -256,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 071374e6cb..b3dab4029e 100644 --- a/packages/integration-react/src/api/ScmAuth.ts +++ b/packages/integration-react/src/api/ScmAuth.ts @@ -53,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`, ); } diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index f8f53e959b..af764c3b69 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/integration +## 0.7.5 + +### Patch Changes + +- 4e1384884f: Fixed bug in integration package where Self Hosted GitLab instances with custom ports weren't supported (because of the lack of an option to add the port in the integration configs. Now users can add the port directly in the host) + +## 0.7.4 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/config@0.1.15 + +## 0.7.3 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/config@0.1.14 + ## 0.7.2 ### Patch Changes @@ -212,7 +235,7 @@ - 0fd4ea443: Updates the `GithubCredentialsProvider` to return the token type, it can either be `token` or `app` depending on the authentication method. - Update the `GithubOrgReaderProcessor` NOT to query for email addresses if GitHub Apps is used for authentication, this is due to inconsistencies in the GitHub API when using server to server communications and installation tokens. https://github.community/t/api-v4-unable-to-retrieve-email-resource-not-accessible-by-integration/13831/4 for more info. + Update the `GithubOrgReaderProcessor` NOT to query for email addresses if GitHub Apps is used for authentication, this is due to inconsistencies in the GitHub API when using server to server communications and installation tokens. See [this community discussion](https://github.community/t/api-v4-unable-to-retrieve-email-resource-not-accessible-by-integration/13831/4) for more info. **Removes** deprecated GithubOrgReaderProcessor provider configuration(`catalog.processors.githubOrg`). If you're using the deprecated config section make sure to migrate to [integrations](https://backstage.io/docs/integrations/github/locations) instead. diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 10da05784c..9a6ba03f8e 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -88,7 +88,7 @@ export class BitbucketIntegration implements ScmIntegration { // @public export type BitbucketIntegrationConfig = { host: string; - apiBaseUrl?: string; + apiBaseUrl: string; token?: string; username?: string; appPassword?: string; diff --git a/packages/integration/package.json b/packages/integration/package.json index d6d0edd77a..bf7c167bfa 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "0.7.2", + "version": "0.7.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -12,6 +12,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,15 +25,15 @@ "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/config": "^0.1.13", + "@backstage/config": "^0.1.15", "cross-fetch": "^3.1.5", "git-url-parse": "^11.6.0", "@octokit/rest": "^18.5.3", @@ -39,9 +42,9 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", - "@backstage/config-loader": "^0.9.3", - "@backstage/test-utils": "^0.2.4-next.0", + "@backstage/cli": "^0.14.1", + "@backstage/config-loader": "^0.9.6", + "@backstage/test-utils": "^0.2.6", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "msw": "^0.35.0" diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts index 81b9be4e25..6bbe1be27b 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts @@ -45,8 +45,10 @@ describe('BitbucketIntegration', () => { expect(integration.title).toBe('h.com'); }); - it('resolves url line number correctly', () => { - const integration = new BitbucketIntegration({ host: 'h.com' } as any); + it('resolves url line number correctly for Bitbucket Cloud', () => { + const integration = new BitbucketIntegration({ + host: 'bitbucket.org', + } as any); expect( integration.resolveUrl({ @@ -55,10 +57,22 @@ describe('BitbucketIntegration', () => { lineNumber: 14, }), ).toBe( - 'https://bitbucket.org/my-owner/my-project/src/master/a.yaml#a.yaml-14', + 'https://bitbucket.org/my-owner/my-project/src/master/a.yaml#lines-14', ); }); + it('resolves url line number correctly for Bitbucket Server', () => { + const integration = new BitbucketIntegration({ host: 'h.com' } as any); + + expect( + integration.resolveUrl({ + url: './a.yaml', + base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md', + lineNumber: 14, + }), + ).toBe('https://bitbucket.org/my-owner/my-project/src/master/a.yaml#14'); + }); + it('resolve edit URL', () => { const integration = new BitbucketIntegration({ host: 'h.com' } as any); diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index 6c21d716b7..0162463e3c 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -60,17 +60,21 @@ export class BitbucketIntegration implements ScmIntegration { lineNumber?: number; }): string { const resolved = defaultScmResolveUrl(options); - - // Bitbucket line numbers use the syntax #example.txt-42, rather than #L42 - if (options.lineNumber) { - const url = new URL(resolved); - - const filename = url.pathname.split('/').slice(-1)[0]; - url.hash = `${filename}-${options.lineNumber}`; - return url.toString(); + if (!options.lineNumber) { + return resolved; } - return resolved; + const url = new URL(resolved); + + if (this.integrationConfig.host === 'bitbucket.org') { + // Bitbucket Cloud uses the syntax #lines-{start}[:{end}][,...] + url.hash = `lines-${options.lineNumber}`; + } else { + // Bitbucket Server uses the syntax #{start}[-{end}][,...] + url.hash = `${options.lineNumber}`; + } + + return url.toString(); } resolveEditUrl(url: string): string { diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts index 1cd911aad3..44a2f0cc1f 100644 --- a/packages/integration/src/bitbucket/config.ts +++ b/packages/integration/src/bitbucket/config.ts @@ -36,12 +36,10 @@ export type BitbucketIntegrationConfig = { * The base URL of the API of this provider, e.g. "https://api.bitbucket.org/2.0", * with no trailing slash. * - * May be omitted specifically for Bitbucket Cloud; then it will be deduced. - * - * The API will always be preferred if both its base URL and a token are - * present. + * Values omitted at the optional property at the app-config will be deduced + * from the "host" value. */ - apiBaseUrl?: string; + apiBaseUrl: string; /** * The authorization token to use for requests to a Bitbucket Server provider. @@ -90,6 +88,8 @@ export function readBitbucketIntegrationConfig( apiBaseUrl = trimEnd(apiBaseUrl, '/'); } else if (host === BITBUCKET_HOST) { apiBaseUrl = BITBUCKET_API_BASE_URL; + } else { + apiBaseUrl = `https://${host}/rest/api/1.0`; } return { diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index f74b849c74..71b4cd868f 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -82,11 +82,7 @@ export function readGitLabIntegrationConfig( baseUrl = `https://${host}`; } - if (host.includes(':')) { - throw new Error( - `Invalid GitLab integration config, host '${host}' should just be the host name (e.g. "github.com"), not a URL`, - ); - } else if (!isValidHost(host)) { + if (!isValidHost(host)) { throw new Error( `Invalid GitLab integration config, '${host}' is not a valid host`, ); diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index c5b0713340..b379d3d129 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -138,7 +138,7 @@ export async function getProjectId( // Convert // to: https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo const repoIDLookup = new URL( - `${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent( + `${url.origin}/api/v4/projects/${encodeURIComponent( repo.replace(/^\//, ''), )}`, ); diff --git a/packages/integration/src/helpers.test.ts b/packages/integration/src/helpers.test.ts index a69c7faec9..60a2789f05 100644 --- a/packages/integration/src/helpers.test.ts +++ b/packages/integration/src/helpers.test.ts @@ -24,7 +24,10 @@ import { describe('basicIntegrations', () => { describe('byUrl', () => { it('handles hosts without a port', () => { - const integration = new BitbucketIntegration({ host: 'host.com' }); + const integration = new BitbucketIntegration({ + host: 'host.com', + apiBaseUrl: 'a', + }); const integrations = basicIntegrations( [integration], i => i.config.host, @@ -33,7 +36,10 @@ describe('basicIntegrations', () => { expect(integrations.byUrl('https://host.com:8080/a')).toBeUndefined(); }); it('handles hosts with a port', () => { - const integration = new BitbucketIntegration({ host: 'host.com:8080' }); + const integration = new BitbucketIntegration({ + host: 'host.com:8080', + apiBaseUrl: 'a', + }); const integrations = basicIntegrations( [integration], i => i.config.host, diff --git a/packages/release-manifests/CHANGELOG.md b/packages/release-manifests/CHANGELOG.md index 61e6c45992..61113e1c76 100644 --- a/packages/release-manifests/CHANGELOG.md +++ b/packages/release-manifests/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/release-manifests +## 0.0.2 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` + ## 0.0.1 ### Patch Changes diff --git a/packages/release-manifests/package.json b/packages/release-manifests/package.json index 8ddbb73bca..7ec69a35e9 100644 --- a/packages/release-manifests/package.json +++ b/packages/release-manifests/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/release-manifests", "description": "Helper library for receiving release manifests", - "version": "0.0.1", + "version": "0.0.2", "private": false, "main": "src/index.ts", "types": "src/index.ts", @@ -11,6 +11,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,18 +25,18 @@ ], "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { "cross-fetch": "^3.1.5" }, "devDependencies": { - "@backstage/test-utils": "^0.2.3", + "@backstage/test-utils": "^0.2.5", "msw": "^0.35.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" diff --git a/packages/search-common/CHANGELOG.md b/packages/search-common/CHANGELOG.md index 4460374646..71f2952d90 100644 --- a/packages/search-common/CHANGELOG.md +++ b/packages/search-common/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/search-common +## 0.2.4 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/types@0.1.3 + - @backstage/plugin-permission-common@0.5.1 + +## 0.2.3 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/plugin-permission-common@0.5.0 + - @backstage/types@0.1.2 + ## 0.2.2 ### Patch Changes diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md index 65f9f25180..475d85e11c 100644 --- a/packages/search-common/api-report.md +++ b/packages/search-common/api-report.md @@ -3,38 +3,33 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { JsonObject } from '@backstage/types'; import { Permission } from '@backstage/plugin-permission-common'; +import { Readable } from 'stream'; +import { Transform } from 'stream'; +import { Writable } from 'stream'; -// Warning: (ae-missing-release-tag) "DocumentCollator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export interface DocumentCollator { - // (undocumented) - execute(): Promise; +// @beta +export interface DocumentCollatorFactory { + getCollator(): Promise; readonly type: string; readonly visibilityPermission?: Permission; } -// Warning: (ae-missing-release-tag) "DocumentDecorator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export interface DocumentDecorator { - // (undocumented) - execute(documents: IndexableDocument[]): Promise; +// @beta +export interface DocumentDecoratorFactory { + getDecorator(): Promise; readonly types?: string[]; } -// Warning: (ae-missing-release-tag) "DocumentTypeInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export type DocumentTypeInfo = { visibilityPermission?: Permission; }; -// Warning: (ae-missing-release-tag) "IndexableDocument" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export interface IndexableDocument { authorization?: { resourceRef: string; @@ -44,23 +39,17 @@ export interface IndexableDocument { title: string; } -// Warning: (ae-missing-release-tag) "QueryRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta export type QueryRequestOptions = { token?: string; }; -// Warning: (ae-missing-release-tag) "QueryTranslator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export type QueryTranslator = (query: SearchQuery) => unknown; -// Warning: (ae-missing-release-tag) "SearchEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export interface SearchEngine { - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; query( query: SearchQuery, options?: QueryRequestOptions, @@ -68,9 +57,7 @@ export interface SearchEngine { setTranslator(translator: QueryTranslator): void; } -// Warning: (ae-missing-release-tag) "SearchQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) export interface SearchQuery { // (undocumented) filters?: JsonObject; @@ -82,9 +69,7 @@ export interface SearchQuery { types?: string[]; } -// Warning: (ae-missing-release-tag) "SearchResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) export interface SearchResult { // (undocumented) document: IndexableDocument; @@ -92,9 +77,7 @@ export interface SearchResult { type: string; } -// Warning: (ae-missing-release-tag) "SearchResultSet" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) export interface SearchResultSet { // (undocumented) nextPageCursor?: string; diff --git a/packages/search-common/package.json b/packages/search-common/package.json index 10b03ab663..ef56017405 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "0.2.2", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -25,22 +28,22 @@ "dist" ], "scripts": { - "build": "backstage-cli build --outputs cjs,types", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "bugs": { "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/types": "^0.1.1", - "@backstage/plugin-permission-common": "^0.4.0-next.0" + "@backstage/types": "^0.1.3", + "@backstage/plugin-permission-common": "^0.5.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0" + "@backstage/cli": "^0.14.0" }, "jest": { "roots": [ diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index 4e61767ea0..51ce45617b 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -16,7 +16,11 @@ import { Permission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; +import { Readable, Transform, Writable } from 'stream'; +/** + * @beta + */ export interface SearchQuery { term: string; filters?: JsonObject; @@ -24,11 +28,17 @@ export interface SearchQuery { pageCursor?: string; } +/** + * @beta + */ export interface SearchResult { type: string; document: IndexableDocument; } +/** + * @beta + */ export interface SearchResultSet { results: SearchResult[]; nextPageCursor?: string; @@ -38,6 +48,7 @@ export interface SearchResultSet { /** * Base properties that all indexed documents must include, as well as some * common properties that documents are encouraged to use where appropriate. + * @beta */ export interface IndexableDocument { /** @@ -72,6 +83,7 @@ export interface IndexableDocument { * Information about a specific document type. Intended to be used in the * {@link @backstage/search-backend-node#IndexBuilder} to collect information * about the types stored in the index. + * @beta */ export type DocumentTypeInfo = { /** @@ -82,10 +94,10 @@ export type DocumentTypeInfo = { }; /** - * Interface that must be implemented in order to expose new documents to - * search. + * Factory class for instantiating collators. + * @beta */ -export interface DocumentCollator { +export interface DocumentCollatorFactory { /** * The type or name of the document set returned by this collator. Used as an * index name by Search Engines. @@ -98,29 +110,41 @@ export interface DocumentCollator { */ readonly visibilityPermission?: Permission; - execute(): Promise; + /** + * Instantiates and resolves a document collator. + */ + getCollator(): Promise; } /** - * Interface that must be implemented in order to decorate existing documents with - * additional metadata. + * Factory class for instantiating decorators. + * @beta */ -export interface DocumentDecorator { +export interface DocumentDecoratorFactory { /** * An optional array of document/index types on which this decorator should * be applied. If no types are provided, this decorator will be applied to * all document/index types. */ readonly types?: string[]; - execute(documents: IndexableDocument[]): Promise; + + /** + * Instantiates and resolves a document decorator. + */ + getDecorator(): Promise; } /** * A type of function responsible for translating an abstract search query into * a concrete query relevant to a particular search engine. + * @beta */ export type QueryTranslator = (query: SearchQuery) => unknown; +/** + * Options when querying a search engine. + * @beta + */ export type QueryRequestOptions = { token?: string; }; @@ -129,6 +153,7 @@ export type QueryRequestOptions = { * Interface that must be implemented by specific search engines, responsible * for performing indexing and querying and translating abstract queries into * concrete, search engine-specific queries. + * @beta */ export interface SearchEngine { /** @@ -137,9 +162,15 @@ export interface SearchEngine { setTranslator(translator: QueryTranslator): void; /** - * Add the given documents to the SearchEngine index of the given type. + * Factory method for getting a search engine indexer for a given document + * type. + * + * @param type - The type or name of the document set for which an indexer + * should be retrieved. This corresponds to the `type` property on the + * document collator/decorator factories and will most often be used to + * identify an index or group to which documents should be written. */ - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; /** * Perform a search query against the SearchEngine. diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 38726ce10e..1302d1ca9d 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,39 @@ # techdocs-cli-embedded-app +## 0.2.65 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@0.9.0 + - @backstage/cli@0.14.1 + - @backstage/core-components@0.8.10 + - @backstage/plugin-techdocs@0.14.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/app-defaults@0.1.9 + - @backstage/core-app-api@0.5.4 + - @backstage/integration-react@0.1.23 + - @backstage/test-utils@0.2.6 + +## 0.2.64 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.14.0 + - @backstage/core-app-api@0.5.3 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/integration-react@0.1.22 + - @backstage/test-utils@0.2.5 + - @backstage/plugin-catalog@0.8.0 + - @backstage/plugin-techdocs@0.13.4 + - @backstage/app-defaults@0.1.8 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + ## 0.2.63 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 975bfef43b..6719701378 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,21 +1,24 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.63", + "version": "0.2.65", "private": true, + "backstage": { + "role": "frontend" + }, "bundled": true, "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", + "@backstage/app-defaults": "^0.1.9", + "@backstage/catalog-model": "^0.11.0", + "@backstage/cli": "^0.14.1", + "@backstage/config": "^0.1.15", + "@backstage/core-app-api": "^0.5.4", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/integration-react": "^0.1.23", + "@backstage/plugin-catalog": "^0.9.0", + "@backstage/plugin-techdocs": "^0.14.0", + "@backstage/test-utils": "^0.2.6", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "history": "^5.0.0", @@ -26,7 +29,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -34,16 +37,16 @@ "@types/node": "^14.14.32", "@types/react-dom": "*", "cross-env": "^7.0.0", - "cypress": "^7.3.0", + "cypress": "^9.5.0", "eslint-plugin-cypress": "^2.10.3", "start-server-and-test": "^1.10.11" }, "scripts": { - "start": "backstage-cli app:serve --config ./app-config.yaml", - "build": "backstage-cli app:build --config ./app-config.yaml", - "clean": "backstage-cli clean", - "test": "backstage-cli test", - "lint": "backstage-cli lint", + "start": "backstage-cli package start --config ./app-config.yaml", + "build": "backstage-cli package build --config ./app-config.yaml", + "clean": "backstage-cli package clean", + "test": "backstage-cli package test", + "lint": "backstage-cli package lint", "test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev", "test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run", "cy:dev": "cypress open", diff --git a/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx b/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx index 65e75a488a..249394fdc4 100644 --- a/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx +++ b/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx @@ -14,18 +14,20 @@ * limitations under the License. */ -import React, { useContext, PropsWithChildren } from 'react'; +import React, { PropsWithChildren, useContext } from 'react'; + import { Link, makeStyles } from '@material-ui/core'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import LogoFull from './LogoFull'; import LogoIcon from './LogoIcon'; + import { Sidebar, + SidebarItem, SidebarPage, sidebarConfig, - SidebarContext, - SidebarItem, SidebarDivider, + SidebarContext, } from '@backstage/core-components'; import { NavLink } from 'react-router-dom'; @@ -70,7 +72,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( {/* Global nav, not org-specific */} {/* End global nav */} diff --git a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx index b90a49fb22..d9a518c165 100644 --- a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx +++ b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx @@ -14,7 +14,22 @@ * limitations under the License. */ -import React from 'react'; +import React, { + FC, + createContext, + useContext, + useState, + useCallback, +} from 'react'; + +import { Theme, makeStyles } from '@material-ui/core'; + +import { ThemeProvider, Box, Tooltip, IconButton } from '@material-ui/core'; +import LightIcon from '@material-ui/icons/Brightness7'; +import DarkIcon from '@material-ui/icons/Brightness4'; + +import { lightTheme, darkTheme } from '@backstage/theme'; +import { EntityName } from '@backstage/catalog-model'; import { Content } from '@backstage/core-components'; @@ -24,6 +39,106 @@ import { TechDocsPageHeader, } from '@backstage/plugin-techdocs'; +const useStyles = makeStyles((theme: Theme) => ({ + headerIcon: { + color: theme.palette.common.white, + width: '32px', + height: '32px', + }, + content: { + backgroundColor: theme.palette.background.default, + }, + contentToolbar: { + display: 'flex', + justifyContent: 'flex-end', + padding: 0, + }, +})); + +enum Themes { + LIGHT = 'light', + DARK = 'dark', +} + +type TechDocsThemeValue = { + theme: Themes; + toggleTheme: () => void; +}; + +const TechDocsThemeContext = createContext({ + theme: Themes.LIGHT, + toggleTheme: () => {}, +}); + +const TechdocsThemeProvider: FC = ({ children }) => { + const [theme, setTheme] = useState(Themes.LIGHT); + + const toggleTheme = useCallback(() => { + setTheme(prevTheme => + prevTheme === Themes.LIGHT ? Themes.DARK : Themes.LIGHT, + ); + }, [setTheme]); + + const value = { theme, toggleTheme }; + + const themes = { + [Themes.LIGHT]: lightTheme, + [Themes.DARK]: darkTheme, + }; + + return ( + + {children} + + ); +}; + +const useTechDocsTheme = () => useContext(TechDocsThemeContext); + +const TechDocsThemeToggle = () => { + const classes = useStyles(); + const { theme, toggleTheme } = useTechDocsTheme(); + + const themes = { + [Themes.LIGHT]: { + icon: DarkIcon, + title: 'Dark theme', + }, + [Themes.DARK]: { + icon: LightIcon, + title: 'Light theme', + }, + }; + + const { title, icon: Icon } = themes[theme]; + + return ( + + + + + + + + ); +}; + +const TechDocsPageContent = ({ + onReady, + entityRef, +}: { + entityRef: EntityName; + onReady: () => void; +}) => { + const classes = useStyles(); + + return ( + + + + ); +}; + const DefaultTechDocsPage = () => { const techDocsMetadata = { site_name: 'Live preview environment', @@ -35,20 +150,20 @@ const DefaultTechDocsPage = () => { {({ entityRef, onReady }) => ( <> - - - + techDocsMetadata={techDocsMetadata} + > + + + )} ); }; -export const techDocsPage = ; +export const techDocsPage = ( + + + +); diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 98015f766a..bc23499662 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,31 @@ # @techdocs/cli +## 0.8.15 + +### Patch Changes + +- ed78516480: chore(deps-dev): bump `cypress` from 7.3.0 to 9.5.0 +- 209fd128e6: Updated usage of `github:` location types in docs to use `url:` instead. +- 61ff215e08: - Adds `cypress` and `cypress-plugin-snapshots` as dependencies for integration and visual regression tests. + - Updates README documentation with instructions for how to run tests. + - Clarifies output text for prepack script. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/techdocs-common@0.11.10 + +## 0.8.14 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/catalog-model@0.10.0 + - @backstage/cli-common@0.1.7 + - @backstage/config@0.1.14 + - @backstage/techdocs-common@0.11.8 + ## 0.8.13 ### Patch Changes diff --git a/packages/techdocs-cli/README.md b/packages/techdocs-cli/README.md index 3ac423cb38..ae296d2c26 100644 --- a/packages/techdocs-cli/README.md +++ b/packages/techdocs-cli/README.md @@ -17,7 +17,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 techdocs-cli-embedded-app and copy the output into the cli dist directory -yarn build --scope @techdocs/cli +yarn workspace @techdocs/cli build # Now execute the binary packages/techdocs-cli/bin/techdocs-cli @@ -40,8 +40,25 @@ yarn start yarn techdocs-cli:dev [...options] ``` +### Using an example docs project + +For the purpose of local development, we have created an example documentation project. You are of course also free to create your own local test site - all it takes is a `docs/index.md` and an `mkdocs.yml` in a directory. + +```sh + +cd packages/techdocs-cli/src/example-docs + +# To get a view of your docs in Backstage, use: +techdocs-cli serve + +# To view the raw mkdocs site (without Backstage), use: +techdocs-cli serve:mkdocs +``` + ### Testing +#### E2E tests + Running unit tests requires mkdocs to be installed locally: ```sh @@ -51,18 +68,37 @@ pip install mkdocs-techdocs-core Then run `yarn test`. -### Use an example docs project +#### Cypress (Integration and Visual regression) tests -We have created an [example documentation project](https://github.com/backstage/techdocs-container/tree/main/mock-docs) and it's shipped with [techdocs-container](https://github.com/backstage/techdocs-container) repository, for the purpose of local development. But you are free to create your own local test site. All it takes is a `docs/index.md` and `mkdocs.yml` in a directory. +Running cypress tests requires you to run the CLI locally against our example docs. + +Run the local version of techdocs-cli against the example docs: ```sh -git clone https://github.com/backstage/techdocs-container.git +# From the root of this repository run +# NOTE: This will build the techdocs-cli-embedded-app and copy the output into the cli dist directory +yarn build --scope @techdocs/cli -cd techdocs-container/mock-docs +# Navigate to the example project +cd packages/techdocs-cli/src/example-docs -# To get a view of your docs in Backstage, use: -techdocs-cli serve - -# To view the raw mkdocs site (without Backstage), use: -techdocs-cli serve:mkdocs +# Now execute the techdocs-cli serve command +../../bin/techdocs-cli serve ``` + +In another shell, run the cypress tests: + +```sh +# From the root of the project, navigate to the techdocs-cli package +cd packages/techdocs-cli + +# Run tests +yarn test:cypress +``` + +This will launch a cypress app where you can run the two different tests: + +- `backstage_serve` - will run against the backstage server +- `mkdocs_serve` - will run test against the mkdocs server + +> If its the first time you run Cypress, it will run a "Verifying Cypress can run" step. This step can result in a "Cypress verification timed out" error. If that is the case, let the verification step run and then run the command again and it should succeed. diff --git a/packages/techdocs-cli/cypress.json b/packages/techdocs-cli/cypress.json new file mode 100644 index 0000000000..b6f3b96019 --- /dev/null +++ b/packages/techdocs-cli/cypress.json @@ -0,0 +1,17 @@ +{ + "env": { + "mkDocsBaseUrl": "http://localhost:8000", + "backstageBaseUrl": "http://localhost:3000", + "cypress-plugin-snapshots": { + "autoCleanUp": false, + "imageConfig": { + "resizeDevicePixelRatio": true, + "threshold": 0.01 + } + } + }, + "viewportWidth": 1920, + "viewportHeight": 1080, + "includeShadowDom": true, + "ignoreTestFiles": ["**/__snapshots__/*", "**/__image_snapshots__/*"] +} diff --git a/packages/techdocs-cli/cypress/.eslintrc.json b/packages/techdocs-cli/cypress/.eslintrc.json new file mode 100644 index 0000000000..2481ac5715 --- /dev/null +++ b/packages/techdocs-cli/cypress/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "plugins": ["cypress"], + "extends": ["plugin:cypress/recommended"], + "rules": { + "jest/expect-expect": [ + "error", + { + "assertFunctionNames": ["expect", "cy.contains", "cy.document"] + } + ], + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": true, + "optionalDependencies": true, + "peerDependencies": true, + "bundledDependencies": true + } + ] + } +} diff --git a/packages/techdocs-cli/cypress/fixtures/example.json b/packages/techdocs-cli/cypress/fixtures/example.json new file mode 100644 index 0000000000..02e4254378 --- /dev/null +++ b/packages/techdocs-cli/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png b/packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png new file mode 100644 index 0000000000..99fc1ea483 Binary files /dev/null and b/packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png differ diff --git a/packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png b/packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png new file mode 100644 index 0000000000..e6c63d5d57 Binary files /dev/null and b/packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png differ diff --git a/packages/techdocs-cli/cypress/integration/backstage_serve.js b/packages/techdocs-cli/cypress/integration/backstage_serve.js new file mode 100644 index 0000000000..15fc03f8f4 --- /dev/null +++ b/packages/techdocs-cli/cypress/integration/backstage_serve.js @@ -0,0 +1,58 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// +describe('TechDocs Live Preview - Backstage Serve', () => { + it('successfully serves documentation', () => { + cy.visit(`${Cypress.env('backstageBaseUrl')}/docs/default/component/local`); + cy.contains('hello mock docs'); + }); + + it('successfully navigates to sub page of documentation', () => { + cy.contains('SubDocs').click(); + cy.contains('Home 2').click(); + cy.contains( + 'This is an md file in another docs folder using the MkDocs Monorepo Plugin', + ); + }); + + it('successfully renders all Backstage main elements', () => { + cy.contains('header', 'Live preview environment'); + cy.get('[data-testid="sidebar-root"]') + .children() + .should('have.length.gt', 0); + }); + + it('successfully renders all extracted MkDocs main elements', () => { + // as it gets replaced by Backstage header + cy.get('.md-header').should('have.length', 0); + cy.get('.md-main').should('have.length', 1); + cy.contains( + '.md-main', + 'This is an md file in another docs folder using the MkDocs Monorepo Plugin', + ); + cy.get('.md-sidebar.md-sidebar--primary').should('have.length', 1); + cy.get('.md-sidebar.md-sidebar--primary').should('have.length', 1); + cy.get('.md-footer').should('have.length', 1); + }); + + it('toMatchImageSnapshot - Backstage TechDocs Page', () => { + cy.visit( + `${Cypress.env('backstageBaseUrl')}/docs/default/component/local`, + ).then(() => { + cy.document().toMatchImageSnapshot(); + }); + }); +}); diff --git a/packages/techdocs-cli/cypress/integration/mkdocs_serve.js b/packages/techdocs-cli/cypress/integration/mkdocs_serve.js new file mode 100644 index 0000000000..1ace7298ce --- /dev/null +++ b/packages/techdocs-cli/cypress/integration/mkdocs_serve.js @@ -0,0 +1,48 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// +describe('TechDocs Live Preview - MkDocs Serve', () => { + it('successfully serves documentation', () => { + cy.visit(Cypress.env('mkDocsBaseUrl')); + cy.contains('hello mock docs'); + }); + + it('successfully navigates to sub page of documentation', () => { + cy.contains('SubDocs').click(); + cy.contains('Home 2').click(); + cy.contains( + 'This is an md file in another docs folder using the MkDocs Monorepo Plugin', + ); + }); + + it('successfully renders all main elements', () => { + cy.get('.md-header').should('have.length', 1); + cy.get('.md-main').should('have.length', 1); + cy.contains( + '.md-main', + 'This is an md file in another docs folder using the MkDocs Monorepo Plugin', + ); + cy.get('.md-sidebar.md-sidebar--primary').should('have.length', 1); + cy.get('.md-sidebar.md-sidebar--primary').should('have.length', 1); + cy.get('.md-footer').should('have.length', 1); + }); + + it('toMatchImageSnapshot - MkDocs Page', () => { + cy.visit(Cypress.env('mkDocsBaseUrl')).then(() => { + cy.document().toMatchImageSnapshot(); + }); + }); +}); diff --git a/packages/techdocs-cli/cypress/plugins/index.js b/packages/techdocs-cli/cypress/plugins/index.js new file mode 100644 index 0000000000..92faabb6ad --- /dev/null +++ b/packages/techdocs-cli/cypress/plugins/index.js @@ -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. + */ + +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +const { initPlugin } = require('cypress-plugin-snapshots/plugin'); + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + initPlugin(on, config); + return config; +}; diff --git a/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage Header #0.png b/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage Header #0.png new file mode 100644 index 0000000000..867e8ddd81 Binary files /dev/null and b/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage Header #0.png differ diff --git a/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0 (1).png b/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0 (1).png new file mode 100644 index 0000000000..3f1078345d Binary files /dev/null and b/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0 (1).png differ diff --git a/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0 (2).png b/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0 (2).png new file mode 100644 index 0000000000..c576be51c5 Binary files /dev/null and b/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0 (2).png differ diff --git a/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png b/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png new file mode 100644 index 0000000000..3f1078345d Binary files /dev/null and b/packages/techdocs-cli/cypress/screenshots/backstage_serve.js/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png differ diff --git a/packages/techdocs-cli/cypress/support/commands.js b/packages/techdocs-cli/cypress/support/commands.js new file mode 100644 index 0000000000..a8d6a5fdb7 --- /dev/null +++ b/packages/techdocs-cli/cypress/support/commands.js @@ -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. + */ + +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/packages/techdocs-cli/cypress/support/index.js b/packages/techdocs-cli/cypress/support/index.js new file mode 100644 index 0000000000..a1a5b10ea4 --- /dev/null +++ b/packages/techdocs-cli/cypress/support/index.js @@ -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. + */ + +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; +import 'cypress-plugin-snapshots/commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 19514b63d2..929564e01c 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,11 +1,14 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "0.8.13", + "version": "0.8.15", "private": false, "publishConfig": { "access": "public" }, + "backstage": { + "role": "cli" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,19 +24,20 @@ "types": "", "scripts": { "start": "nodemon --", - "build": "backstage-cli build --outputs cjs", - "clean": "backstage-cli clean", - "lint": "backstage-cli lint", - "test": "backstage-cli test --testPathIgnorePatterns=src/e2e.test.ts", + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "test": "backstage-cli package 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:cypress": "cypress open", "prepack": "./scripts/prepack.sh" }, "bin": { "techdocs-cli": "bin/techdocs-cli" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/commander": "^2.12.2", "@types/fs-extra": "^9.0.6", "@types/http-proxy": "^1.17.4", @@ -41,9 +45,11 @@ "@types/node": "^14.14.32", "@types/serve-handler": "^6.1.0", "@types/webpack-env": "^1.15.3", - "techdocs-cli-embedded-app": "link:../techdocs-cli-embedded-app", + "cypress": "^9.5.0", + "cypress-plugin-snapshots": "^1.4.4", "find-process": "^1.4.5", "nodemon": "^2.0.2", + "techdocs-cli-embedded-app": "link:../techdocs-cli-embedded-app", "ts-node": "^10.0.0" }, "files": [ @@ -56,11 +62,11 @@ "ext": "ts" }, "dependencies": { - "@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.7", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-model": "^0.11.0", + "@backstage/cli-common": "^0.1.8", + "@backstage/config": "^0.1.15", + "@backstage/techdocs-common": "^0.11.10", "@types/dockerode": "^3.3.0", "commander": "^6.1.0", "dockerode": "^3.3.1", diff --git a/packages/techdocs-cli/scripts/prepack.sh b/packages/techdocs-cli/scripts/prepack.sh index 6b769de37c..bfd62cfd1b 100755 --- a/packages/techdocs-cli/scripts/prepack.sh +++ b/packages/techdocs-cli/scripts/prepack.sh @@ -23,4 +23,4 @@ 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!" +echo "🏁 Finished copying embedded app into dist/embedded-app!" diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index a255805139..0f5213df34 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -39,6 +39,7 @@ export default async function generate(cmd: Command) { const sourceDir = resolve(cmd.sourceDir); const outputDir = resolve(cmd.outputDir); + const omitTechdocsCorePlugin = cmd.omitTechdocsCoreMkdocsPlugin; const dockerImage = cmd.dockerImage; const pullImage = cmd.pull; @@ -55,6 +56,9 @@ export default async function generate(cmd: Command) { runIn: cmd.docker ? 'docker' : 'local', dockerImage, pullImage, + mkdocs: { + omitTechdocsCorePlugin, + }, }, }, }); diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index d4f41cf7a3..6f1d9847e1 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -45,7 +45,7 @@ export function registerCommands(program: CommanderStatic) { ) .option( '--techdocs-ref ', - 'The repository hosting documentation source files e.g. github:https://ghe.mycompany.net.com/org/repo.' + + 'The repository hosting documentation source files e.g. url:https://ghe.mycompany.net.com/org/repo.' + '\nThis value is same as the backstage.io/techdocs-ref annotation of the corresponding Backstage entity.' + '\nIt is completely fine to skip this as it is only being used to set repo_url in mkdocs.yml if not found.\n', ) @@ -54,6 +54,11 @@ export function registerCommands(program: CommanderStatic) { 'A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json.', ) .option('-v --verbose', 'Enable verbose output.', false) + .option( + '--omitTechdocsCoreMkdocsPlugin', + "Don't patch MkDocs file automatically with techdocs-core plugin.", + false, + ) .alias('build') .action(lazy(() => import('./generate/generate').then(m => m.default))); diff --git a/packages/techdocs-cli/src/e2e.test.ts b/packages/techdocs-cli/src/e2e.test.ts index 088dbc44b8..7cdd867d0f 100644 --- a/packages/techdocs-cli/src/e2e.test.ts +++ b/packages/techdocs-cli/src/e2e.test.ts @@ -58,7 +58,7 @@ const timeout = 25000; jest.setTimeout(timeout * 2); describe('end-to-end', () => { - const cwd = path.resolve(__dirname, 'fixture'); + const cwd = path.resolve(__dirname, 'example-docs'); afterEach(async () => { // On Windows the pid of a spawned process may be wrong diff --git a/packages/techdocs-cli/src/example-docs/.gitignore b/packages/techdocs-cli/src/example-docs/.gitignore new file mode 100644 index 0000000000..45ddf0ae39 --- /dev/null +++ b/packages/techdocs-cli/src/example-docs/.gitignore @@ -0,0 +1 @@ +site/ diff --git a/packages/techdocs-cli/src/example-docs/docs/index.md b/packages/techdocs-cli/src/example-docs/docs/index.md new file mode 100644 index 0000000000..074d3ccb3e --- /dev/null +++ b/packages/techdocs-cli/src/example-docs/docs/index.md @@ -0,0 +1,92 @@ +## hello mock docs + +!!! test +Testing something + +Abbreviations: +Some text about MOCDOC + +This is a paragraph. +{: #test_id .test_class } + +Apple +: A fruit. + +```javascript +import { test } from 'something'; + +const addThingToThing = (a, b) a + b; +``` + +- [abc](#abc) +- [xyz](#xyz) + +## abc + +This is a b c. + +## xyz + +This is x y z. + +# Emojis + +:bulb: :smile: + +# Code blocks + +```javascript +import { test } from 'something'; + +const addThingToThing = (a, b) a + b; +``` + +# Grouped Code blocks + +=== "JavaScript" + + ```javascript + import { test } from 'something'; + + const addThingToThing = (a, b) a + b; + ``` + +=== "Java" + + ```java + public void function() { + test(); + } + ``` + +```java tab="java" + public void function() { + test(); + } +``` + +```java tab="java 2" + public void function() { + test(); + } +``` + +# MDX truly sane lists + +- attributes + +- customer + - first_name + - test + - family_name + - email +- person + - first_name + - family_name + - birth_date +- subscription_id + +- request + + +*[MOCDOC]: Mock Documentation diff --git a/packages/techdocs-cli/src/fixture/mkdocs.yml b/packages/techdocs-cli/src/example-docs/mkdocs.yml similarity index 50% rename from packages/techdocs-cli/src/fixture/mkdocs.yml rename to packages/techdocs-cli/src/example-docs/mkdocs.yml index 5d5c2a02ae..9c9406f6e6 100644 --- a/packages/techdocs-cli/src/fixture/mkdocs.yml +++ b/packages/techdocs-cli/src/example-docs/mkdocs.yml @@ -2,7 +2,9 @@ site_name: docs-test-fixture site_description: Documentation site test fixture nav: - - HOME: README.md + - Home: index.md + - SubDocs: '!include ./sub-docs/mkdocs.yml' + - Plugins: '*include ./plugins/*/mkdocs.yml' plugins: - techdocs-core diff --git a/packages/techdocs-cli/src/example-docs/plugins/plugin-a/docs/index.md b/packages/techdocs-cli/src/example-docs/plugins/plugin-a/docs/index.md new file mode 100644 index 0000000000..bd8bc3a4ea --- /dev/null +++ b/packages/techdocs-cli/src/example-docs/plugins/plugin-a/docs/index.md @@ -0,0 +1,4 @@ +# Plugin A + +This is a description of Plugin A. This file exists to prove that glob'd +includes using the `*include` syntax work as expected. diff --git a/packages/techdocs-cli/src/example-docs/plugins/plugin-a/mkdocs.yml b/packages/techdocs-cli/src/example-docs/plugins/plugin-a/mkdocs.yml new file mode 100644 index 0000000000..66407d5391 --- /dev/null +++ b/packages/techdocs-cli/src/example-docs/plugins/plugin-a/mkdocs.yml @@ -0,0 +1,5 @@ +site_name: Plugin A +site_description: A description of Plugin A + +nav: + - Introduction: index.md diff --git a/packages/techdocs-cli/src/example-docs/plugins/plugin-b/docs/index.md b/packages/techdocs-cli/src/example-docs/plugins/plugin-b/docs/index.md new file mode 100644 index 0000000000..93e545252f --- /dev/null +++ b/packages/techdocs-cli/src/example-docs/plugins/plugin-b/docs/index.md @@ -0,0 +1,4 @@ +# Plugin B + +This is a description of Plugin B. This file exists to prove that glob'd +includes using the `*include` syntax work as expected. diff --git a/packages/techdocs-cli/src/example-docs/plugins/plugin-b/mkdocs.yml b/packages/techdocs-cli/src/example-docs/plugins/plugin-b/mkdocs.yml new file mode 100644 index 0000000000..81580c33af --- /dev/null +++ b/packages/techdocs-cli/src/example-docs/plugins/plugin-b/mkdocs.yml @@ -0,0 +1,5 @@ +site_name: Plugin B +site_description: A description of Plugin B + +nav: + - Introduction: index.md diff --git a/packages/techdocs-cli/src/example-docs/sub-docs/docs/index.md b/packages/techdocs-cli/src/example-docs/sub-docs/docs/index.md new file mode 100644 index 0000000000..65c6644ef8 --- /dev/null +++ b/packages/techdocs-cli/src/example-docs/sub-docs/docs/index.md @@ -0,0 +1 @@ +### This is an md file in another docs folder using the [MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin) diff --git a/packages/techdocs-cli/src/example-docs/sub-docs/mkdocs.yml b/packages/techdocs-cli/src/example-docs/sub-docs/mkdocs.yml new file mode 100644 index 0000000000..4490ddbefa --- /dev/null +++ b/packages/techdocs-cli/src/example-docs/sub-docs/mkdocs.yml @@ -0,0 +1,4 @@ +site_name: subdocs + +nav: + - Home 2: 'index.md' diff --git a/packages/techdocs-cli/src/fixture/docs/README.md b/packages/techdocs-cli/src/fixture/docs/README.md deleted file mode 100644 index c32f73f6f4..0000000000 --- a/packages/techdocs-cli/src/fixture/docs/README.md +++ /dev/null @@ -1 +0,0 @@ -# Test Fixture diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 44fbec4171..c99295e8db 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/techdocs-common +## 0.11.10 + +### Patch Changes + +- 209fd128e6: Updated usage of `github:` location types in docs to use `url:` instead. +- 13ef228d03: Clean up the API interface for TechDocs common library. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/integration@0.7.5 + +## 0.11.9 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + - @backstage/search-common@0.2.4 + +## 0.11.8 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 216725b434: Updated to use new names for `parseLocationRef` and `stringifyLocationRef` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/search-common@0.2.3 + ## 0.11.7 ### Patch Changes diff --git a/packages/techdocs-common/README.md b/packages/techdocs-common/README.md index e4889d6f79..bbaac85e9b 100644 --- a/packages/techdocs-common/README.md +++ b/packages/techdocs-common/README.md @@ -39,7 +39,7 @@ Currently the build process is split up in these three stages. - Generators - Publishers -Preparers read your entity data and creates a working directory with your documentation source code. For example if you have set your `backstage.io/techdocs-ref` to `github:https://github.com/backstage/backstage.git` it will clone that repository to a temp folder and pass that on to the generator. +Preparers read your entity data and creates a working directory with your documentation source code. For example if you have set your `backstage.io/techdocs-ref` to `url:https://github.com/backstage/backstage.git` it will clone that repository to a temp folder and pass that on to the generator. Generators takes the prepared source and runs the `techdocs-container` on it. It then passes on the output folder of that build to the publisher. diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index 281accca30..7c4fc95abb 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -17,38 +17,37 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; -// Warning: (ae-missing-release-tag) "DirectoryPreparer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class DirectoryPreparer implements PreparerBase { - constructor(config: Config, _logger: Logger_2, reader: UrlReader); - // Warning: (ae-forgotten-export) The symbol "PreparerResponse" needs to be exported by the entry point index.d.ts - // - // (undocumented) - prepare( - entity: Entity, - options?: { - logger?: Logger_2; - etag?: string; - }, - ): Promise; + // @deprecated + constructor(config: Config, _logger: Logger_2 | null, reader: UrlReader); + static fromConfig( + config: Config, + { logger, reader }: PreparerConfig, + ): DirectoryPreparer; + prepare(entity: Entity, options?: PreparerOptions): Promise; } -// Warning: (ae-missing-release-tag) "GeneratorBase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type ETag = string; + +// @public export type GeneratorBase = { run(opts: GeneratorRunOptions): Promise; }; -// Warning: (ae-missing-release-tag) "GeneratorBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type GeneratorBuilder = { register(protocol: SupportedGeneratorKey, generator: GeneratorBase): void; get(entity: Entity): GeneratorBase; }; +// @public +export type GeneratorOptions = { + containerRunner: ContainerRunner; + logger: Logger_2; +}; + // @public export type GeneratorRunOptions = { inputDir: string; @@ -59,11 +58,8 @@ export type GeneratorRunOptions = { logStream?: Writable; }; -// Warning: (ae-missing-release-tag) "Generators" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class Generators implements GeneratorBuilder { - // (undocumented) static fromConfig( config: Config, options: { @@ -71,15 +67,11 @@ export class Generators implements GeneratorBuilder { containerRunner: ContainerRunner; }, ): Promise; - // (undocumented) get(entity: Entity): GeneratorBase; - // (undocumented) register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase): void; } -// Warning: (ae-missing-release-tag) "getDocFilesFromRepository" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const getDocFilesFromRepository: ( reader: UrlReader, entity: Entity, @@ -91,78 +83,74 @@ export const getDocFilesFromRepository: ( | undefined, ) => Promise; -// Warning: (ae-missing-release-tag) "getLocationForEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const getLocationForEntity: ( entity: Entity, scmIntegration: ScmIntegrationRegistry, ) => ParsedLocationAnnotation; -// Warning: (ae-missing-release-tag) "ParsedLocationAnnotation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type MigrateRequest = { + removeOriginal?: boolean; + concurrency?: number; +}; + +// @public export type ParsedLocationAnnotation = { type: RemoteProtocol; target: string; }; -// Warning: (ae-missing-release-tag) "parseReferenceAnnotation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const parseReferenceAnnotation: ( annotationName: string, entity: Entity, ) => ParsedLocationAnnotation; -// Warning: (ae-missing-release-tag) "PreparerBase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PreparerBase = { - prepare( - entity: Entity, - options?: { - logger?: Logger_2; - etag?: string; - }, - ): Promise; + prepare(entity: Entity, options?: PreparerOptions): Promise; }; -// Warning: (ae-missing-release-tag) "PreparerBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PreparerBuilder = { register(protocol: RemoteProtocol, preparer: PreparerBase): void; get(entity: Entity): PreparerBase; }; -// Warning: (ae-missing-release-tag) "Preparers" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type PreparerConfig = { + logger: Logger_2; + reader: UrlReader; +}; + +// @public +export type PreparerOptions = { + logger?: Logger_2; + etag?: ETag; +}; + +// @public +export type PreparerResponse = { + preparedDir: string; + etag: ETag; +}; + +// @public export class Preparers implements PreparerBuilder { - // Warning: (ae-forgotten-export) The symbol "factoryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) static fromConfig( - config: Config, - { logger, reader }: factoryOptions, + backstageConfig: Config, + { logger, reader }: PreparerConfig, ): Promise; - // (undocumented) get(entity: Entity): PreparerBase; - // (undocumented) register(protocol: RemoteProtocol, preparer: PreparerBase): void; } -// Warning: (ae-missing-release-tag) "Publisher" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class Publisher { - // Warning: (ae-forgotten-export) The symbol "factoryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) static fromConfig( config: Config, - { logger, discovery }: factoryOptions_2, + { logger, discovery }: PublisherFactory, ): Promise; } @@ -172,15 +160,16 @@ export interface PublisherBase { fetchTechDocsMetadata(entityName: EntityName): Promise; getReadiness(): Promise; hasDocsBeenGenerated(entityName: Entity): Promise; - // Warning: (ae-forgotten-export) The symbol "MigrateRequest" needs to be exported by the entry point index.d.ts migrateDocsCase?(migrateRequest: MigrateRequest): Promise; - // Warning: (ae-forgotten-export) The symbol "PublishRequest" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "PublishResponse" needs to be exported by the entry point index.d.ts publish(request: PublishRequest): Promise; } -// Warning: (ae-missing-release-tag) "PublisherType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type PublisherFactory = { + logger: Logger_2; + discovery: PluginEndpointDiscovery; +}; + // @public export type PublisherType = | 'local' @@ -189,37 +178,40 @@ export type PublisherType = | 'azureBlobStorage' | 'openStackSwift'; +// @public +export type PublishRequest = { + entity: Entity; + directory: string; +}; + +// @public +export type PublishResponse = { + remoteUrl?: string; + objects?: string[]; +} | void; + // @public export type ReadinessResponse = { isAvailable: boolean; }; -// Warning: (ae-missing-release-tag) "RemoteProtocol" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type RemoteProtocol = 'url' | 'dir'; -// Warning: (ae-missing-release-tag) "TechDocsDocument" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type SupportedGeneratorKey = 'techdocs' | string; + +// @public export interface TechDocsDocument extends IndexableDocument { - // (undocumented) kind: string; - // (undocumented) lifecycle: string; - // (undocumented) name: string; - // (undocumented) namespace: string; - // (undocumented) owner: string; - // (undocumented) path: string; } -// Warning: (ae-missing-release-tag) "TechdocsGenerator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class TechdocsGenerator implements GeneratorBase { constructor(options: { logger: Logger_2; @@ -227,21 +219,14 @@ export class TechdocsGenerator implements GeneratorBase { config: Config; scmIntegrations: ScmIntegrationRegistry; }); - static readonly defaultDockerImage = 'spotify/techdocs:v0.3.6'; - // (undocumented) + static readonly defaultDockerImage = 'spotify/techdocs:v0.3.7'; static fromConfig( config: Config, - options: { - containerRunner: ContainerRunner; - logger: Logger_2; - }, + options: GeneratorOptions, ): TechdocsGenerator; - // (undocumented) run(options: GeneratorRunOptions): Promise; } -// Warning: (ae-missing-release-tag) "TechDocsMetadata" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type TechDocsMetadata = { site_name: string; @@ -251,8 +236,6 @@ export type TechDocsMetadata = { files?: string[]; }; -// Warning: (ae-missing-release-tag) "transformDirLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const transformDirLocation: ( entity: Entity, @@ -263,21 +246,11 @@ export const transformDirLocation: ( target: string; }; -// Warning: (ae-missing-release-tag) "UrlPreparer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class UrlPreparer implements PreparerBase { + // @deprecated constructor(reader: UrlReader, logger: Logger_2); - // (undocumented) - prepare( - entity: Entity, - options?: { - etag?: string; - }, - ): Promise; + static fromConfig({ reader, logger }: PreparerConfig): UrlPreparer; + prepare(entity: Entity, options?: PreparerOptions): Promise; } - -// Warnings were encountered during analysis: -// -// src/stages/generate/types.d.ts:45:5 - (ae-forgotten-export) The symbol "SupportedGeneratorKey" needs to be exported by the entry point index.d.ts ``` diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index f45f842100..731e967637 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.7", + "version": "0.11.10", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "node-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -25,12 +28,13 @@ "dist" ], "scripts": { - "build": "backstage-cli build --outputs cjs,types", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "bugs": { "url": "https://github.com/backstage/backstage/issues" @@ -38,12 +42,12 @@ "dependencies": { "@azure/identity": "^2.0.1", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.10.7", - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", - "@backstage/search-common": "^0.2.2", - "@backstage/integration": "^0.7.2", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/search-common": "^0.2.4", + "@backstage/integration": "^0.7.5", "@google-cloud/storage": "^5.6.0", "@trendyol-js/openstack-swift-sdk": "^0.0.5", "@types/express": "^4.17.6", @@ -60,7 +64,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@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..39a1f4c666 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'; @@ -26,11 +26,21 @@ import path from 'path'; import { Logger } from 'winston'; import { PreparerResponse, RemoteProtocol } from './stages/prepare/types'; +/** + * Parsed location annotation + * @public + */ export type ParsedLocationAnnotation = { type: RemoteProtocol; target: string; }; +/** + * Returns a parset locations annotation + * @public + * @param annotationName - The name of the annotation in the entity metadata + * @param entity - A TechDocs entity instance + */ export const parseReferenceAnnotation = ( annotationName: string, entity: Entity, @@ -42,7 +52,7 @@ export const parseReferenceAnnotation = ( ); } - const { type, target } = parseLocationReference(annotation); + const { type, target } = parseLocationRef(annotation); return { type: type as RemoteProtocol, target, @@ -56,7 +66,7 @@ export const parseReferenceAnnotation = ( * location, it returns a `url` location with a resolved target that points to the * targeted subfolder. If the entity was registered by a `file` location, it returns * an absolute `dir` location. - * + * @public * @param entity - the entity with annotations * @param dirAnnotation - the parsed techdocs-ref annotation of type 'dir' * @param scmIntegrations - access to the scmIntegration to do url transformations @@ -101,6 +111,12 @@ export const transformDirLocation = ( } }; +/** + * Returns a entity reference based on the TechDocs annotation type + * @public + * @param entity - A TechDocs instance + * @param scmIntegration - An implementation for SCM integration API + */ export const getLocationForEntity = ( entity: Entity, scmIntegration: ScmIntegrationRegistry, @@ -120,6 +136,13 @@ export const getLocationForEntity = ( } }; +/** + * Returns a preparer response {@link PreparerResponse} + * @public + * @param reader - Read a tree of files from a repository + * @param entity - A TechDocs entity instance + * @param opts - Options for configuring the reader, e.g. logger, etag, etc. + */ export const getDocFilesFromRepository = async ( reader: UrlReader, entity: Entity, diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml new file mode 100644 index 0000000000..09e8fd7ac7 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml @@ -0,0 +1,6 @@ +site_name: Test site name +site_description: Test site description +docs_dir: docs/ +plugins: + - not-techdocs-core + - also-not-techdocs-core diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml new file mode 100644 index 0000000000..eea9a8a3d9 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml @@ -0,0 +1,5 @@ +site_name: Test site name +site_description: Test site description +# This is a comment that is removed after editing +plugins: + - techdocs-core diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml new file mode 100644 index 0000000000..e75b06ada7 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml @@ -0,0 +1,3 @@ +site_name: Test site name +site_description: Test site description +docs_dir: docs/ diff --git a/packages/techdocs-common/src/stages/generate/generators.ts b/packages/techdocs-common/src/stages/generate/generators.ts index de39d4e7c3..630fccfa44 100644 --- a/packages/techdocs-common/src/stages/generate/generators.ts +++ b/packages/techdocs-common/src/stages/generate/generators.ts @@ -26,9 +26,18 @@ import { SupportedGeneratorKey, } from './types'; +/** + * Collection of docs generators + * @public + */ export class Generators implements GeneratorBuilder { private generatorMap = new Map(); + /** + * Returns a generators instance containing a generator for TechDocs + * @param config - A Backstage configuration + * @param options - Options to configure the TechDocs generator + */ static async fromConfig( config: Config, options: { logger: Logger; containerRunner: ContainerRunner }, @@ -41,10 +50,19 @@ export class Generators implements GeneratorBuilder { return generators; } + /** + * Register a generator in the generators collection + * @param generatorKey - Unique identifier for the generator + * @param generator - The generator instance to register + */ register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase) { this.generatorMap.set(generatorKey, generator); } + /** + * Returns the generator for a given TechDocs entity + * @param entity - A TechDocs entity instance + */ get(entity: Entity): GeneratorBase { const generatorKey = getGeneratorKey(entity); const generator = this.generatorMap.get(generatorKey); diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 9323ab4f18..7590df94fb 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -28,10 +28,14 @@ import { getMkdocsYml, getRepoUrlFromLocationAnnotation, patchIndexPreBuild, - patchMkdocsYmlPreBuild, storeEtagMetadata, validateMkdocsYaml, } from './helpers'; +import { + patchMkdocsYmlPreBuild, + pathMkdocsYmlWithTechdocsPlugin, +} from './mkDocsPatchers'; +import yaml from 'js-yaml'; const mockEntity = { apiVersion: 'version', @@ -65,6 +69,15 @@ const mkdocsYmlWithInvalidDocDir2 = fs.readFileSync( const mkdocsYmlWithComments = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_comments.yml'), ); +const mkdocsYmlWithTechdocsPlugins = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_techdocs_plugin.yml'), +); +const mkdocsYmlWithoutPlugins = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_without_plugins.yml'), +); +const mkdocsYmlWithAdditionalPlugins = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_additional_plugins.yml'), +); const mockLogger = getVoidLogger(); const warn = jest.spyOn(mockLogger, 'warn'); @@ -289,6 +302,60 @@ describe('helpers', () => { }); }); + describe('pathMkdocsYmlWithTechdocsPlugin', () => { + beforeEach(() => { + mockFs({ + '/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins, + '/mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins, + '/mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins, + }); + }); + it('should not add additional plugins if techdocs exists already in mkdocs file', async () => { + await pathMkdocsYmlWithTechdocsPlugin( + '/mkdocs_with_techdocs_plugin.yml', + mockLogger, + ); + + const updatedMkdocsYml = await fs.readFile( + '/mkdocs_with_techdocs_plugin.yml', + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(1); + expect(parsedYml.plugins).toContain('techdocs-core'); + }); + it("should add the needed plugin if it doesn't exist in mkdocs file", async () => { + await pathMkdocsYmlWithTechdocsPlugin( + '/mkdocs_without_plugins.yml', + mockLogger, + ); + + const updatedMkdocsYml = await fs.readFile('/mkdocs_without_plugins.yml'); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(1); + expect(parsedYml.plugins).toContain('techdocs-core'); + }); + it('should not override existing plugins', async () => { + await pathMkdocsYmlWithTechdocsPlugin( + '/mkdocs_with_additional_plugins.yml', + mockLogger, + ); + const updatedMkdocsYml = await fs.readFile( + '/mkdocs_with_additional_plugins.yml', + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(3); + expect(parsedYml.plugins).toContain('techdocs-core'); + expect(parsedYml.plugins).toContain('not-techdocs-core'); + expect(parsedYml.plugins).toContain('also-not-techdocs-core'); + }); + }); + describe('patchIndexPreBuild', () => { afterEach(() => { warn.mockClear(); diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 2debd5822c..349783d2bd 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -125,7 +125,7 @@ class UnknownTag { constructor(public readonly data: any, public readonly type?: string) {} } -const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([ +export const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([ new Type('', { kind: 'scalar', multi: true, @@ -203,101 +203,6 @@ export const validateMkdocsYaml = async ( return parsedMkdocsYml.docs_dir; }; -/** - * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. - * - * List of tasks: - * - Add repo_url or edit_uri if it does not exists - * If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default. - * If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get - * the repository URL. - * - * This function will not throw an error since this is not critical to the whole TechDocs pipeline. - * Instead it will log warnings if there are any errors in reading, parsing or writing YAML. - * - * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site - * @param logger - A logger instance - * @param parsedLocationAnnotation - Object with location url and type - * @param scmIntegrations - the scmIntegration to do url transformations - */ -export const patchMkdocsYmlPreBuild = async ( - mkdocsYmlPath: string, - logger: Logger, - parsedLocationAnnotation: ParsedLocationAnnotation, - scmIntegrations: ScmIntegrationRegistry, -) => { - // We only want to override the mkdocs.yml if it has actually changed. This is relevant if - // used with a 'dir' location on the file system as this would permanently update the file. - let didEdit = false; - - let mkdocsYmlFileString; - try { - mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); - } catch (error) { - assertError(error); - logger.warn( - `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`, - ); - return; - } - - let mkdocsYml: any; - try { - mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA }); - - // mkdocsYml should be an object type after successful parsing. - // But based on its type definition, it can also be a string or undefined, which we don't want. - if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') { - throw new Error('Bad YAML format.'); - } - } catch (error) { - assertError(error); - logger.warn( - `Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`, - ); - return; - } - - // Add edit_uri and/or repo_url to mkdocs.yml if it is missing. - // This will enable the Page edit button generated by MkDocs. - // If the either has been set, keep the original value - if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) { - const result = getRepoUrlFromLocationAnnotation( - parsedLocationAnnotation, - scmIntegrations, - mkdocsYml.docs_dir, - ); - - if (result.repo_url || result.edit_uri) { - mkdocsYml.repo_url = result.repo_url; - mkdocsYml.edit_uri = result.edit_uri; - didEdit = true; - - logger.info( - `Set ${JSON.stringify( - result, - )}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`, - ); - } - } - - try { - if (didEdit) { - await fs.writeFile( - mkdocsYmlPath, - yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), - 'utf8', - ); - } - } catch (error) { - assertError(error); - logger.warn( - `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`, - ); - return; - } -}; - /** * Update docs/index.md file before TechDocs generator uses it to generate docs site, * falling back to docs/README.md or README.md in case a default docs/index.md diff --git a/packages/techdocs-common/src/stages/generate/index.ts b/packages/techdocs-common/src/stages/generate/index.ts index eb015a5c2d..1c20c58887 100644 --- a/packages/techdocs-common/src/stages/generate/index.ts +++ b/packages/techdocs-common/src/stages/generate/index.ts @@ -16,7 +16,9 @@ export { TechdocsGenerator } from './techdocs'; export { Generators } from './generators'; export type { - GeneratorBuilder, GeneratorBase, + GeneratorOptions, + GeneratorBuilder, GeneratorRunOptions, + SupportedGeneratorKey, } from './types'; diff --git a/packages/techdocs-common/src/stages/generate/mkDocsPatchers.ts b/packages/techdocs-common/src/stages/generate/mkDocsPatchers.ts new file mode 100644 index 0000000000..d03b5d83c2 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/mkDocsPatchers.ts @@ -0,0 +1,166 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Logger } from 'winston'; +import fs from 'fs-extra'; +import yaml from 'js-yaml'; +import { ParsedLocationAnnotation } from '../../helpers'; +import { getRepoUrlFromLocationAnnotation, MKDOCS_SCHEMA } from './helpers'; +import { assertError } from '@backstage/errors'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +type MkDocsObject = { + plugins?: string[]; + docs_dir: string; + repo_url?: string; + edit_uri?: string; +}; + +const patchMkdocsFile = async ( + mkdocsYmlPath: string, + logger: Logger, + updateAction: (mkdocsYml: MkDocsObject) => boolean, +) => { + // We only want to override the mkdocs.yml if it has actually changed. This is relevant if + // used with a 'dir' location on the file system as this would permanently update the file. + let didEdit = false; + + let mkdocsYmlFileString; + try { + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + } catch (error) { + assertError(error); + logger.warn( + `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`, + ); + return; + } + + let mkdocsYml: any; + try { + mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA }); + + // mkdocsYml should be an object type after successful parsing. + // But based on its type definition, it can also be a string or undefined, which we don't want. + if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') { + throw new Error('Bad YAML format.'); + } + } catch (error) { + assertError(error); + logger.warn( + `Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`, + ); + return; + } + + didEdit = updateAction(mkdocsYml); + + try { + if (didEdit) { + await fs.writeFile( + mkdocsYmlPath, + yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), + 'utf8', + ); + } + } catch (error) { + assertError(error); + logger.warn( + `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`, + ); + return; + } +}; + +/** + * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. + * + * List of tasks: + * - Add repo_url or edit_uri if it does not exists + * If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default. + * If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get + * the repository URL. + * + * This function will not throw an error since this is not critical to the whole TechDocs pipeline. + * Instead it will log warnings if there are any errors in reading, parsing or writing YAML. + * + * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site + * @param logger - A logger instance + * @param parsedLocationAnnotation - Object with location url and type + * @param scmIntegrations - the scmIntegration to do url transformations + */ +export const patchMkdocsYmlPreBuild = async ( + mkdocsYmlPath: string, + logger: Logger, + parsedLocationAnnotation: ParsedLocationAnnotation, + scmIntegrations: ScmIntegrationRegistry, +) => { + await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => { + if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) { + // Add edit_uri and/or repo_url to mkdocs.yml if it is missing. + // This will enable the Page edit button generated by MkDocs. + // If the either has been set, keep the original value + const result = getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + mkdocsYml.docs_dir, + ); + + if (result.repo_url || result.edit_uri) { + mkdocsYml.repo_url = result.repo_url; + mkdocsYml.edit_uri = result.edit_uri; + + logger.info( + `Set ${JSON.stringify( + result, + )}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`, + ); + return true; + } + } + return false; + }); +}; + +/** + * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. + * + * List of tasks: + * - Add techdocs-core plugin to mkdocs file if it doesn't exist + * + * This function will not throw an error since this is not critical to the whole TechDocs pipeline. + * Instead it will log warnings if there are any errors in reading, parsing or writing YAML. + * + * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site + * @param logger - A logger instance + */ +export const pathMkdocsYmlWithTechdocsPlugin = async ( + mkdocsYmlPath: string, + logger: Logger, +) => { + await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => { + // Modify mkdocs.yaml to contain the needed techdocs-core plugin if it is not there + if (!('plugins' in mkdocsYml)) { + mkdocsYml.plugins = ['techdocs-core']; + return true; + } + + if (mkdocsYml.plugins && !mkdocsYml.plugins.includes('techdocs-core')) { + mkdocsYml.plugins.push('techdocs-core'); + return true; + } + return false; + }); +}; diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 5cf992092c..b46f52f99a 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -26,34 +26,45 @@ import { createOrUpdateMetadata, getMkdocsYml, patchIndexPreBuild, - patchMkdocsYmlPreBuild, runCommand, storeEtagMetadata, validateMkdocsYaml, } from './helpers'; + +import { + patchMkdocsYmlPreBuild, + pathMkdocsYmlWithTechdocsPlugin, +} from './mkDocsPatchers'; import { GeneratorBase, GeneratorConfig, + GeneratorOptions, GeneratorRunInType, GeneratorRunOptions, } from './types'; import { ForwardedError } from '@backstage/errors'; +/** + * Generates documentation files + * @public + */ export class TechdocsGenerator implements GeneratorBase { /** * The default docker image (and version) used to generate content. Public * and static so that techdocs-common consumers can use the same version. */ - public static readonly defaultDockerImage = 'spotify/techdocs:v0.3.6'; + public static readonly defaultDockerImage = 'spotify/techdocs:v0.3.7'; private readonly logger: Logger; private readonly containerRunner: ContainerRunner; private readonly options: GeneratorConfig; private readonly scmIntegrations: ScmIntegrationRegistry; - static fromConfig( - config: Config, - options: { containerRunner: ContainerRunner; logger: Logger }, - ) { + /** + * Returns a instance of TechDocs generator + * @param config - A Backstage configuration + * @param options - Options to configure the generator + */ + static fromConfig(config: Config, options: GeneratorOptions) { const { containerRunner, logger } = options; const scmIntegrations = ScmIntegrations.fromConfig(config); return new TechdocsGenerator({ @@ -76,6 +87,7 @@ export class TechdocsGenerator implements GeneratorBase { this.scmIntegrations = options.scmIntegrations; } + /** {@inheritDoc GeneratorBase.run} */ public async run(options: GeneratorRunOptions): Promise { const { inputDir, @@ -102,6 +114,10 @@ export class TechdocsGenerator implements GeneratorBase { await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir }); } + if (!this.options.omitTechdocsCoreMkdocsPlugin) { + await pathMkdocsYmlWithTechdocsPlugin(mkdocsYmlPath, childLogger); + } + // Directories to bind on container const mountDirs = { [inputDir]: '/input', @@ -199,5 +215,8 @@ export function readGeneratorConfig( 'docker', dockerImage: config.getOptionalString('techdocs.generator.dockerImage'), pullImage: config.getOptionalBoolean('techdocs.generator.pullImage'), + omitTechdocsCoreMkdocsPlugin: config.getOptionalBoolean( + 'techdocs.generator.mkdocs.omitTechdocsCorePlugin', + ), }; } diff --git a/packages/techdocs-common/src/stages/generate/types.ts b/packages/techdocs-common/src/stages/generate/types.ts index 2a090d5584..f46dbdc56a 100644 --- a/packages/techdocs-common/src/stages/generate/types.ts +++ b/packages/techdocs-common/src/stages/generate/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ContainerRunner } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Writable } from 'stream'; import { Logger } from 'winston'; @@ -22,6 +23,15 @@ import { ParsedLocationAnnotation } from '../../helpers'; // Determines where the generator will be run export type GeneratorRunInType = 'docker' | 'local'; +/** + * Options for building generators + * @public + */ +export type GeneratorOptions = { + containerRunner: ContainerRunner; + logger: Logger; +}; + /** * The techdocs generator configurations options. */ @@ -29,6 +39,7 @@ export type GeneratorConfig = { runIn: GeneratorRunInType; dockerImage?: string; pullImage?: boolean; + omitTechdocsCoreMkdocsPlugin?: boolean; }; /** @@ -51,18 +62,27 @@ export type GeneratorRunOptions = { logStream?: Writable; }; +/** + * Generates documentation files + * @public + */ export type GeneratorBase = { - // Runs the generator with the values + /** + * Runs the generator with the values + * @public + */ run(opts: GeneratorRunOptions): Promise; }; /** * List of supported generator options + * @public */ export type SupportedGeneratorKey = 'techdocs' | string; /** * The generator builder holds the generator ready for run time + * @public */ export type GeneratorBuilder = { register(protocol: SupportedGeneratorKey, generator: GeneratorBase): void; diff --git a/packages/techdocs-common/src/stages/prepare/dir.test.ts b/packages/techdocs-common/src/stages/prepare/dir.test.ts index 019bf51fe9..2bf3fe29fc 100644 --- a/packages/techdocs-common/src/stages/prepare/dir.test.ts +++ b/packages/techdocs-common/src/stages/prepare/dir.test.ts @@ -86,7 +86,7 @@ describe('directory preparer', () => { ); }); - it('should reject when managed-by-location is a git repository', async () => { + it('should reject when managed-by-location has an unknown type', async () => { const directoryPreparer = new DirectoryPreparer( mockConfig, logger, @@ -95,12 +95,12 @@ describe('directory preparer', () => { const mockEntity = createMockEntity({ 'backstage.io/managed-by-location': - 'github:https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + 'does-not-exist:https://github.com/backstage/backstage/blob/master/catalog-info.yaml', 'backstage.io/techdocs-ref': 'dir:./docs', }); await expect(directoryPreparer.prepare(mockEntity)).rejects.toThrow( - /Unable to resolve location type github/, + /Unable to resolve location type does-not-exist/, ); }); }); diff --git a/packages/techdocs-common/src/stages/prepare/dir.ts b/packages/techdocs-common/src/stages/prepare/dir.ts index ade0b00162..42bfa5b903 100644 --- a/packages/techdocs-common/src/stages/prepare/dir.ts +++ b/packages/techdocs-common/src/stages/prepare/dir.ts @@ -24,20 +24,43 @@ import { } from '@backstage/integration'; import { Logger } from 'winston'; import { parseReferenceAnnotation, transformDirLocation } from '../../helpers'; -import { PreparerBase, PreparerResponse } from './types'; +import { + PreparerBase, + PreparerConfig, + PreparerOptions, + PreparerResponse, +} from './types'; +/** + * Preparer used to retrieve documentation files from a local directory + * @public + */ export class DirectoryPreparer implements PreparerBase { private readonly scmIntegrations: ScmIntegrationRegistry; private readonly reader: UrlReader; - constructor(config: Config, _logger: Logger, reader: UrlReader) { + /** @deprecated use static fromConfig method instead */ + constructor(config: Config, _logger: Logger | null, reader: UrlReader) { this.reader = reader; this.scmIntegrations = ScmIntegrations.fromConfig(config); } + /** + * Returns a directory preparer instance + * @param config - A backstage config + * @param options - A directory preparer options containing a logger and reader + */ + static fromConfig( + config: Config, + { logger, reader }: PreparerConfig, + ): DirectoryPreparer { + return new DirectoryPreparer(config, logger, reader); + } + + /** {@inheritDoc PreparerBase.prepare} */ async prepare( entity: Entity, - options?: { logger?: Logger; etag?: string }, + options?: PreparerOptions, ): Promise { const annotation = parseReferenceAnnotation( 'backstage.io/techdocs-ref', diff --git a/packages/techdocs-common/src/stages/prepare/index.ts b/packages/techdocs-common/src/stages/prepare/index.ts index f129bf5315..3593b49c86 100644 --- a/packages/techdocs-common/src/stages/prepare/index.ts +++ b/packages/techdocs-common/src/stages/prepare/index.ts @@ -16,4 +16,12 @@ export { DirectoryPreparer } from './dir'; export { UrlPreparer } from './url'; export { Preparers } from './preparers'; -export type { PreparerBuilder, PreparerBase, RemoteProtocol } from './types'; +export type { + PreparerBase, + PreparerBuilder, + PreparerConfig, + PreparerOptions, + PreparerResponse, + RemoteProtocol, + ETag, +} from './types'; diff --git a/packages/techdocs-common/src/stages/prepare/preparers.ts b/packages/techdocs-common/src/stages/prepare/preparers.ts index 75c9a7f23b..575294e91a 100644 --- a/packages/techdocs-common/src/stages/prepare/preparers.ts +++ b/packages/techdocs-common/src/stages/prepare/preparers.ts @@ -13,26 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { Logger } from 'winston'; import { parseReferenceAnnotation } from '../../helpers'; import { DirectoryPreparer } from './dir'; import { UrlPreparer } from './url'; -import { PreparerBase, PreparerBuilder, RemoteProtocol } from './types'; - -type factoryOptions = { - logger: Logger; - reader: UrlReader; -}; +import { + PreparerBase, + PreparerBuilder, + PreparerConfig, + RemoteProtocol, +} from './types'; +/** + * Collection of docs preparers (dir and url) + * @public + */ export class Preparers implements PreparerBuilder { private preparerMap = new Map(); + /** + * Returns a generators instance containing a generator for TechDocs + * @public + * @param backstageConfig - A Backstage configuration + * @param preparerConfig - Options to configure preparers + */ static async fromConfig( - config: Config, - { logger, reader }: factoryOptions, + backstageConfig: Config, + { logger, reader }: PreparerConfig, ): Promise { const preparers = new Preparers(); @@ -43,16 +51,30 @@ export class Preparers implements PreparerBuilder { * Dir preparer is a syntactic sugar for users to define techdocs-ref annotation. * When using dir preparer, the docs will be fetched using URL Reader. */ - const directoryPreparer = new DirectoryPreparer(config, logger, reader); + const directoryPreparer = new DirectoryPreparer( + backstageConfig, + logger, + reader, + ); preparers.register('dir', directoryPreparer); return preparers; } + /** + * Register a preparer in the preparers collection + * @param protocol - url or dir to associate with preparer + * @param preparer - The preparer instance to set + */ register(protocol: RemoteProtocol, preparer: PreparerBase) { this.preparerMap.set(protocol, preparer); } + /** + * Returns the preparer for a given TechDocs entity + * @param entity - A TechDocs entity instance + * @returns + */ get(entity: Entity): PreparerBase { const { type } = parseReferenceAnnotation( 'backstage.io/techdocs-ref', diff --git a/packages/techdocs-common/src/stages/prepare/types.ts b/packages/techdocs-common/src/stages/prepare/types.ts index 5c219752fa..aee9d216ab 100644 --- a/packages/techdocs-common/src/stages/prepare/types.ts +++ b/packages/techdocs-common/src/stages/prepare/types.ts @@ -15,19 +15,58 @@ */ import type { Entity } from '@backstage/catalog-model'; +import { UrlReader } from '@backstage/backend-common'; import { Logger } from 'winston'; +/** + * A unique identifier of the tree blob, usually the commit SHA or etag from the target. + * @public + */ +export type ETag = string; + +/** + * Options for building preparers + * @public + */ +export type PreparerConfig = { + logger: Logger; + reader: UrlReader; +}; + +/** + * Options for configuring the content preparation process. + * @public + */ +export type PreparerOptions = { + /** + * An instance of the logger + */ + logger?: Logger; + /** + * see {@link ETag} + */ + etag?: ETag; +}; + +/** + * Result of the preparation step. + * @public + */ export type PreparerResponse = { /** * The path to directory where the tree is downloaded. */ preparedDir: string; /** - * A unique identifier of the tree blob, usually the commit SHA or etag from the target. + * see {@link ETag} */ - etag: string; + etag: ETag; }; +/** + * Definition of a TechDocs preparer + * @public + */ export type PreparerBase = { /** * Given an Entity definition from the Software Catalog, go and prepare a directory @@ -38,15 +77,20 @@ export type PreparerBase = { * updated since the last build. * @throws `NotModifiedError` when the prepared directory has not been changed since the last build. */ - prepare( - entity: Entity, - options?: { logger?: Logger; etag?: string }, - ): Promise; + prepare(entity: Entity, options?: PreparerOptions): Promise; }; +/** + * Definition for a TechDocs preparer builder + * @public + */ export type PreparerBuilder = { register(protocol: RemoteProtocol, preparer: PreparerBase): void; get(entity: Entity): PreparerBase; }; +/** + * Location where documentation files are stored + * @public + */ export type RemoteProtocol = 'url' | 'dir'; diff --git a/packages/techdocs-common/src/stages/prepare/url.ts b/packages/techdocs-common/src/stages/prepare/url.ts index cd4a8860e4..8026dca41e 100644 --- a/packages/techdocs-common/src/stages/prepare/url.ts +++ b/packages/techdocs-common/src/stages/prepare/url.ts @@ -19,20 +19,39 @@ import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Logger } from 'winston'; import { getDocFilesFromRepository } from '../../helpers'; -import { PreparerBase, PreparerResponse } from './types'; +import { + PreparerBase, + PreparerConfig, + PreparerOptions, + PreparerResponse, +} from './types'; +/** + * Preparer used to retrieve documentation files from a remote repository + * @public + */ export class UrlPreparer implements PreparerBase { private readonly logger: Logger; private readonly reader: UrlReader; + /** @deprecated use static fromConfig method instead */ constructor(reader: UrlReader, logger: Logger) { this.logger = logger; this.reader = reader; } + /** + * Returns a directory preparer instance + * @param config - A URL preparer config containing the a logger and reader + */ + static fromConfig({ reader, logger }: PreparerConfig): UrlPreparer { + return new UrlPreparer(reader, logger); + } + + /** {@inheritDoc PreparerBase.prepare} */ async prepare( entity: Entity, - options?: { etag?: string }, + options?: PreparerOptions, ): Promise { try { return await getDocFilesFromRepository(this.reader, entity, { diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index 3e4c4c705b..357df73c8c 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -15,7 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; @@ -34,7 +34,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name); + return path.join(rootDir, namespace || DEFAULT_NAMESPACE, kind, name); }; const logger = getVoidLogger(); diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts index e6fd45eed7..f1b660555e 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts @@ -15,7 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; @@ -34,7 +34,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name); + return path.join(rootDir, namespace || DEFAULT_NAMESPACE, kind, name); }; const logger = getVoidLogger(); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts index ed35f2e445..87a38402d0 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -15,7 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; @@ -34,7 +34,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name); + return path.join(rootDir, namespace || DEFAULT_NAMESPACE, kind, name); }; const logger = getVoidLogger(); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index ba70c36e27..c3d9d3330b 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -83,7 +83,7 @@ export class GoogleGCSPublish implements PublisherBase { const credentials = config.getOptionalString( 'techdocs.publisher.googleGcs.credentials', ); - let credentialsJson = {}; + let credentialsJson: any = {}; if (credentials) { try { credentialsJson = JSON.parse(credentials); @@ -96,6 +96,7 @@ export class GoogleGCSPublish implements PublisherBase { const storageClient = new Storage({ ...(credentials && { + projectId: credentialsJson.project_id, credentials: credentialsJson, }), }); diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/packages/techdocs-common/src/stages/publish/helpers.test.ts index 6245898586..7acdb47ea7 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.test.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.test.ts @@ -16,7 +16,7 @@ import mockFs from 'mock-fs'; import * as os from 'os'; import * as path from 'path'; -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { getStaleFiles, getFileTreeRecursively, @@ -189,7 +189,7 @@ describe('getCloudPathForLocalPath', () => { localPath, ); expect(remoteBucket).toBe( - `${ENTITY_DEFAULT_NAMESPACE}/component/backstage/${localPath}`, + `${DEFAULT_NAMESPACE}/component/backstage/${localPath}`, ); }); diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts index cbecee51bc..decefae187 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import mime from 'mime-types'; import path from 'path'; import createLimiter from 'p-limit'; @@ -188,9 +188,9 @@ export const getCloudPathForLocalPath = ( const relativeFilePathPosix = localPath.split(path.sep).join(path.posix.sep); // The / delimiter is intentional since it represents the cloud storage and not the local file system. - const entityRootDir = `${ - entity.metadata?.namespace ?? ENTITY_DEFAULT_NAMESPACE - }/${entity.kind}/${entity.metadata.name}`; + const entityRootDir = `${entity.metadata?.namespace ?? DEFAULT_NAMESPACE}/${ + entity.kind + }/${entity.metadata.name}`; const relativeFilePathTriplet = `${entityRootDir}/${relativeFilePathPosix}`; diff --git a/packages/techdocs-common/src/stages/publish/index.ts b/packages/techdocs-common/src/stages/publish/index.ts index 083cf2b8ff..c6f86f6bc1 100644 --- a/packages/techdocs-common/src/stages/publish/index.ts +++ b/packages/techdocs-common/src/stages/publish/index.ts @@ -17,6 +17,10 @@ export { Publisher } from './publish'; export type { PublisherBase, PublisherType, - TechDocsMetadata, + PublisherFactory, + PublishRequest, + PublishResponse, + MigrateRequest, ReadinessResponse, + TechDocsMetadata, } from './types'; diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts index ef06e26cfa..b240ce73da 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts @@ -18,7 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { Entity, EntityName, - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import express from 'express'; @@ -59,7 +59,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name); + return path.join(rootDir, namespace || DEFAULT_NAMESPACE, kind, name); }; const getPosixEntityRootDir = (entity: Entity) => { @@ -70,7 +70,7 @@ const getPosixEntityRootDir = (entity: Entity) => { return path.posix.join( '/rootDir', - namespace || ENTITY_DEFAULT_NAMESPACE, + namespace || DEFAULT_NAMESPACE, kind, name, ); diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/packages/techdocs-common/src/stages/publish/publish.ts index eb9e3104bf..bc7f50b5a3 100644 --- a/packages/techdocs-common/src/stages/publish/publish.ts +++ b/packages/techdocs-common/src/stages/publish/publish.ts @@ -14,29 +14,28 @@ * limitations under the License. */ -import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import { Logger } from 'winston'; import { AwsS3Publish } from './awsS3'; import { AzureBlobStoragePublish } from './azureBlobStorage'; import { GoogleGCSPublish } from './googleStorage'; import { LocalPublish } from './local'; import { OpenStackSwiftPublish } from './openStackSwift'; -import { PublisherBase, PublisherType } from './types'; - -type factoryOptions = { - logger: Logger; - discovery: PluginEndpointDiscovery; -}; +import { PublisherFactory, PublisherBase, PublisherType } from './types'; /** * Factory class to create a TechDocs publisher based on defined publisher type in app config. * Uses `techdocs.publisher.type`. + * @public */ export class Publisher { + /** + * Returns a instance of TechDocs publisher + * @param config - A Backstage configuration + * @param options - Options for configuring the publisher factory + */ static async fromConfig( config: Config, - { logger, discovery }: factoryOptions, + { logger, discovery }: PublisherFactory, ): Promise { const publisherType = (config.getOptionalString( 'techdocs.publisher.type', diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/packages/techdocs-common/src/stages/publish/types.ts index 37075a35a4..c6ad032484 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -14,10 +14,22 @@ * limitations under the License. */ import { Entity, EntityName } from '@backstage/catalog-model'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Logger } from 'winston'; import express from 'express'; +/** + * Options for building publishers + * @public + */ +export type PublisherFactory = { + logger: Logger; + discovery: PluginEndpointDiscovery; +}; + /** * Key for all the different types of TechDocs publishers that are supported. + * @public */ export type PublisherType = | 'local' @@ -26,6 +38,10 @@ export type PublisherType = | 'azureBlobStorage' | 'openStackSwift'; +/** + * Request publish definition + * @public + */ export type PublishRequest = { entity: Entity; /* The Path to the directory where the generated files are stored. */ @@ -35,6 +51,7 @@ export type PublishRequest = { /** * Response containing metadata about where files were published and what may * have been published or updated. + * @public */ export type PublishResponse = { /** @@ -51,7 +68,6 @@ export type PublishResponse = { /** * Result for the validation check. - * * @public */ export type ReadinessResponse = { @@ -62,6 +78,7 @@ export type ReadinessResponse = { /** * Type to hold metadata found in techdocs_metadata.json and associated with each site * @param etag - ETag of the resource used to generate the site. Usually the latest commit sha of the source repository. + * @public */ export type TechDocsMetadata = { site_name: string; @@ -71,6 +88,10 @@ export type TechDocsMetadata = { files?: string[]; }; +/** + * TechDocs entity triplet migration request + * @public + */ export type MigrateRequest = { /** * Whether or not to remove the source file. Defaults to false (acting like a diff --git a/packages/techdocs-common/src/techdocsTypes.ts b/packages/techdocs-common/src/techdocsTypes.ts index 4aa3972b31..0b722f4da5 100644 --- a/packages/techdocs-common/src/techdocsTypes.ts +++ b/packages/techdocs-common/src/techdocsTypes.ts @@ -16,11 +16,33 @@ import { IndexableDocument } from '@backstage/search-common'; +/** + * TechDocs indexable document interface + * @public + */ export interface TechDocsDocument extends IndexableDocument { + /** + * Entity kind + */ kind: string; + /** + * Entity metadata namespace + */ namespace: string; + /** + * Entity metadata name + */ name: string; + /** + * Entity lifecycle + */ lifecycle: string; + /** + * Entity owner + */ owner: string; + /** + * Entity path + */ path: string; } diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 35c9ebfb35..4de90a4aa7 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/test-utils +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.7.0 + - @backstage/core-app-api@0.5.4 + - @backstage/plugin-permission-react@0.3.2 + +## 0.2.5 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-app-api@0.5.3 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-permission-common@0.5.0 + - @backstage/plugin-permission-react@0.3.1 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + - @backstage/types@0.1.2 + ## 0.2.4 ### Patch Changes diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 4e5361ff80..2f2671885e 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -164,8 +164,6 @@ export class MockStorageApi implements StorageApi { // (undocumented) forBucket(name: string): StorageApi; // (undocumented) - get(key: string): T | undefined; - // (undocumented) observe$(key: string): Observable>; // (undocumented) remove(key: string): Promise; diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 67b68e08cc..5465f7783a 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,13 +1,16 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.2.4", + "version": "0.2.6", "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,21 +24,22 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli build --outputs types,esm", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@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", - "@backstage/theme": "^0.2.14", - "@backstage/types": "^0.1.1", + "@backstage/config": "^0.1.15", + "@backstage/core-app-api": "^0.5.4", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/theme": "^0.2.15", + "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.11.2", "@testing-library/jest-dom": "^5.10.1", @@ -51,7 +55,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", + "@backstage/cli": "^0.14.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.35.0" diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts index c974198be1..63aa4235e1 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts @@ -24,7 +24,7 @@ describe('WebStorage Storage API', () => { it('should return undefined for values which are unset', async () => { const storage = createMockStorage(); - expect(storage.get('myfakekey')).toBeUndefined(); + expect(storage.snapshot('myfakekey').value).toBeUndefined(); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'absent', @@ -33,32 +33,29 @@ describe('WebStorage Storage API', () => { }); }); - it('should allow the setting and getting of the simple data structures', async () => { + it('should allow the setting and snapshotting of the simple data structures', async () => { const storage = createMockStorage(); await storage.set('myfakekey', 'helloimastring'); await storage.set('mysecondfakekey', 1234); await storage.set('mythirdfakekey', true); - expect(storage.get('myfakekey')).toBe('helloimastring'); - expect(storage.get('mysecondfakekey')).toBe(1234); - expect(storage.get('mythirdfakekey')).toBe(true); + expect(storage.snapshot('myfakekey').value).toBe('helloimastring'); + expect(storage.snapshot('mysecondfakekey').value).toBe(1234); + expect(storage.snapshot('mythirdfakekey').value).toBe(true); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: 'helloimastring', - newValue: 'helloimastring', }); expect(storage.snapshot('mysecondfakekey')).toEqual({ key: 'mysecondfakekey', presence: 'present', value: 1234, - newValue: 1234, }); expect(storage.snapshot('mythirdfakekey')).toEqual({ key: 'mythirdfakekey', presence: 'present', value: true, - newValue: true, }); }); @@ -72,12 +69,11 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', mockData); - expect(storage.get('myfakekey')).toEqual(mockData); + expect(storage.snapshot('myfakekey').value).toEqual(mockData); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -107,7 +103,6 @@ describe('WebStorage Storage API', () => { key: 'correctKey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -153,9 +148,11 @@ describe('WebStorage Storage API', () => { await firstStorage.set(keyName, 'boop'); await secondStorage.set(keyName, 'deerp'); - expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); - expect(firstStorage.get(keyName)).toBe('boop'); - expect(secondStorage.get(keyName)).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toBe( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName).value).toBe('boop'); + expect(secondStorage.snapshot(keyName).value).toBe('deerp'); expect(firstStorage.snapshot(keyName)).not.toEqual( secondStorage.snapshot(keyName), ); @@ -163,13 +160,11 @@ describe('WebStorage Storage API', () => { key: keyName, presence: 'present', value: 'boop', - newValue: 'boop', }); expect(secondStorage.snapshot(keyName)).toEqual({ key: keyName, presence: 'present', value: 'deerp', - newValue: 'deerp', }); }); @@ -186,7 +181,7 @@ describe('WebStorage Storage API', () => { await firstStorage.set('test2', { error: true }); - expect(secondStorage.get('deep/test2')).toBe(undefined); + expect(secondStorage.snapshot('deep/test2').value).toBe(undefined); expect(secondStorage.snapshot('deep/test2')).toMatchObject({ presence: 'absent', }); @@ -201,19 +196,17 @@ describe('WebStorage Storage API', () => { await firstStorage.set('test2', true); - expect(firstStorage.get('test2')).toBe(true); - expect(secondStorage.get('test2')).toBe(undefined); + expect(firstStorage.snapshot('test2').value).toBe(true); + expect(secondStorage.snapshot('test2').value).toBe(undefined); expect(firstStorage.snapshot('test2')).toEqual({ key: 'test2', presence: 'present', value: true, - newValue: true, }); expect(secondStorage.snapshot('test2')).toEqual({ key: 'test2', presence: 'absent', value: undefined, - newValue: undefined, }); }); diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index 25024f2211..6ce91a4a31 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -61,10 +61,6 @@ export class MockStorageApi implements StorageApi { return this.bucketStorageApis.get(name)!; } - get(key: string): T | undefined { - return this.snapshot(key).value as T | undefined; - } - snapshot(key: string): StorageValueSnapshot { if (this.data.hasOwnProperty(this.getKeyName(key))) { const data = this.data[this.getKeyName(key)]; @@ -72,14 +68,12 @@ export class MockStorageApi implements StorageApi { key, presence: 'present', value: data, - newValue: data, }; } return { key, presence: 'absent', value: undefined, - newValue: undefined, }; } @@ -95,7 +89,6 @@ export class MockStorageApi implements StorageApi { key, presence: 'present', value: serialized, - newValue: serialized, }); } @@ -105,7 +98,6 @@ export class MockStorageApi implements StorageApi { key, presence: 'absent', value: undefined, - newValue: undefined, }); } diff --git a/packages/test-utils/src/testUtils/defaultApis.ts b/packages/test-utils/src/testUtils/defaultApis.ts index f06e1ba6b7..1ff10bfe6f 100644 --- a/packages/test-utils/src/testUtils/defaultApis.ts +++ b/packages/test-utils/src/testUtils/defaultApis.ts @@ -21,16 +21,13 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, - OAuth2, OktaAuth, GitlabAuth, - Auth0Auth, MicrosoftAuth, BitbucketAuth, OAuthRequestManager, WebStorage, UrlPatternDiscovery, - SamlAuth, OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, @@ -45,16 +42,12 @@ import { oauthRequestApiRef, googleAuthApiRef, githubAuthApiRef, - oauth2ApiRef, oktaAuthApiRef, gitlabAuthApiRef, - auth0AuthApiRef, microsoftAuthApiRef, storageApiRef, configApiRef, - samlAuthApiRef, oneloginAuthApiRef, - oidcAuthApiRef, bitbucketAuthApiRef, atlassianAuthApiRef, } from '@backstage/core-plugin-api'; @@ -158,46 +151,6 @@ export const defaultApis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: auth0AuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oauth2ApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: samlAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => - SamlAuth.create({ - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: oneloginAuthApiRef, deps: { @@ -212,25 +165,6 @@ export const defaultApis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: oidcAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'oidc', - title: 'Your Identity Provider', - icon: () => null, - }, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: bitbucketAuthApiRef, deps: { diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index cfa6ee3633..5c3e8cf5d6 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/theme +## 0.2.15 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` + ## 0.2.14 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index 2846db2805..17404e8db0 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,13 +1,16 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.2.14", + "version": "0.2.15", "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,17 +24,19 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli build --outputs types,esm", - "lint": "backstage-cli lint", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start", + "test": "backstage-cli package test" }, "dependencies": { "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0" + "@backstage/cli": "^0.14.0" }, "files": [ "dist" diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md new file mode 100644 index 0000000000..238c61f68f --- /dev/null +++ b/packages/types/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/types + +## 0.1.3 + +### Patch Changes + +- Fix for the previous release with missing type declarations. + +## 0.1.2 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` diff --git a/packages/types/package.json b/packages/types/package.json index ae18fe519d..ab0740d568 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/types", "description": "Common TypeScript types used within Backstage", - "version": "0.1.1", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -12,6 +12,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,16 +25,16 @@ "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": {}, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", + "@backstage/cli": "^0.14.0", "@types/zen-observable": "^0.8.0", "zen-observable": "^0.8.15" }, diff --git a/packages/version-bridge/CHANGELOG.md b/packages/version-bridge/CHANGELOG.md index e079cd6c61..fe131f7987 100644 --- a/packages/version-bridge/CHANGELOG.md +++ b/packages/version-bridge/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/version-bridge +## 0.1.2 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` + ## 0.1.1 ### Patch Changes diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 6989aec3cf..485b0417cf 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -1,13 +1,16 @@ { "name": "@backstage/version-bridge", "description": "Utilities used by @backstage packages to support multiple concurrent versions", - "version": "0.1.1", + "version": "0.1.2", "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,19 +24,20 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli build --outputs types,esm", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", + "@backstage/cli": "^0.14.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-backend/.eslintrc.js b/plugins/airbrake-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/airbrake-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md new file mode 100644 index 0000000000..148dbdeb07 --- /dev/null +++ b/plugins/airbrake-backend/CHANGELOG.md @@ -0,0 +1,25 @@ +# @backstage/plugin-airbrake-backend + +## 0.2.0 + +### Minor Changes + +- da78e79a94: This marks the first release where the Airbrake plugin is useable. Airbrake frontend and Airbrake backend work with each other. Currently just a summary of the latest Airbrakes is shown on Backstage. + +### Patch Changes + +- 0045270527: Make config optional +- Updated dependencies + - @backstage/backend-common@0.11.0 + +## 0.1.0 + +### Minor Changes + +- a5aebe3e30: Created the Airbrake backend plugin that proxies requests to the Airbrake API + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/config@0.1.14 diff --git a/plugins/airbrake-backend/README.md b/plugins/airbrake-backend/README.md new file mode 100644 index 0000000000..21056229de --- /dev/null +++ b/plugins/airbrake-backend/README.md @@ -0,0 +1,32 @@ +# airbrake-backend + +The Airbrake backend plugin provides a simple proxy to the Airbrake API while hiding away the secret API key from the frontend. + +## How to use + +See the [Airbrake plugin instructions](../airbrake/README.md#how-to-use). + +## Local Development + +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. + +1. Add the required config to your `app-config.local.yaml`: + + ```yaml + airbrake: + apiKey: ${AIRBRAKE_API_KEY} + ``` + +2. Set the environment variable `AIRBRAKE_API_KEY` with your [API + key](https://airbrake.io/docs/api/#authentication). You can also write it + directly into the config file above for convenience - but beware of + accidentally leaking the key. + +3. Go into the plugin's directory and run it in standalone mode by running `yarn start`. + +Access it from http://localhost:7007/api/airbrake. Or use the [Airbrake plugin in standalone mode](../airbrake/README.md#local-development) which will talk to it automatically. + +Here are some example endpoints: + +- http://localhost:7007/api/airbrake/health +- http://localhost:7007/api/airbrake/api/v4/projects diff --git a/plugins/airbrake-backend/api-report.md b/plugins/airbrake-backend/api-report.md new file mode 100644 index 0000000000..b9e9a50e0b --- /dev/null +++ b/plugins/airbrake-backend/api-report.md @@ -0,0 +1,26 @@ +## API Report File for "@backstage/plugin-airbrake-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import express from 'express'; +import { Logger as Logger_2 } from 'winston'; + +// @public +export interface AirbrakeConfig { + apiKey: string; +} + +// @public +export function createRouter(options: RouterOptions): Promise; + +// @public +export function extractAirbrakeConfig(config: Config): AirbrakeConfig; + +// @public +export interface RouterOptions { + airbrakeConfig: AirbrakeConfig; + logger: Logger_2; +} +``` diff --git a/plugins/airbrake-backend/config.d.ts b/plugins/airbrake-backend/config.d.ts new file mode 100644 index 0000000000..a21f609550 --- /dev/null +++ b/plugins/airbrake-backend/config.d.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** Configuration options for the Airbrake plugin */ + airbrake?: { + /** + * The API Key to authenticate requests. More details on how to get this + * here: https://airbrake.io/docs/api/#authentication + * + * @visibility secret + */ + apiKey: string; + }; +} diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json new file mode 100644 index 0000000000..2ee36eefb7 --- /dev/null +++ b/plugins/airbrake-backend/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/plugin-airbrake-backend", + "version": "0.2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "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" + }, + "dependencies": { + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@types/express": "*", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "http-proxy-middleware": "^2.0.0", + "winston": "^3.2.1", + "cross-fetch": "^3.0.6", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.14.1", + "@types/http-proxy-middleware": "^0.19.3", + "@types/supertest": "^2.0.8", + "supertest": "^6.1.6", + "msw": "^0.35.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/airbrake-backend/src/config/ExtractAirbrakeConfig.test.ts b/plugins/airbrake-backend/src/config/ExtractAirbrakeConfig.test.ts new file mode 100644 index 0000000000..53d744b9ee --- /dev/null +++ b/plugins/airbrake-backend/src/config/ExtractAirbrakeConfig.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { extractAirbrakeConfig } from './ExtractAirbrakeConfig'; + +describe('ExtractAirbrakeConfig', () => { + it('gets the API key', () => { + const config = new ConfigReader({ + airbrake: { + apiKey: 'fakeApiKey', + }, + }); + + const airbrakeConfig = extractAirbrakeConfig(config); + expect(airbrakeConfig.apiKey).toStrictEqual('fakeApiKey'); + }); + + it('fails for missing keys', () => { + expect(() => extractAirbrakeConfig(new ConfigReader({}))).toThrow(); + expect(() => + extractAirbrakeConfig(new ConfigReader({ airbrake: {} })), + ).toThrow(); + }); +}); diff --git a/plugins/airbrake-backend/src/config/ExtractAirbrakeConfig.ts b/plugins/airbrake-backend/src/config/ExtractAirbrakeConfig.ts new file mode 100644 index 0000000000..de6a31feec --- /dev/null +++ b/plugins/airbrake-backend/src/config/ExtractAirbrakeConfig.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 { Config } from '@backstage/config'; + +/** + * The configuration needed for the airbrake-backend plugin + * + * @public + */ +export interface AirbrakeConfig { + /** + * The API Key to authenticate requests. More details on how to get this here: https://airbrake.io/docs/api/#authentication + */ + apiKey: string; +} + +/** + * Extract the Airbrake config from a config object + * + * @public + * + * @param config - The config object to extract from + */ +export function extractAirbrakeConfig(config: Config): AirbrakeConfig { + return { + apiKey: config.getString('airbrake.apiKey'), + }; +} diff --git a/plugins/airbrake-backend/src/config/index.ts b/plugins/airbrake-backend/src/config/index.ts new file mode 100644 index 0000000000..60d60c2761 --- /dev/null +++ b/plugins/airbrake-backend/src/config/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ExtractAirbrakeConfig'; diff --git a/plugins/airbrake-backend/src/index.ts b/plugins/airbrake-backend/src/index.ts new file mode 100644 index 0000000000..f4ec6008c2 --- /dev/null +++ b/plugins/airbrake-backend/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The Airbrake Backend plugin used by \@backstage/plugin-airbrake + * + * @packageDocumentation + */ + +export * from './service/router'; +export * from './config'; diff --git a/plugins/airbrake-backend/src/run.ts b/plugins/airbrake-backend/src/run.ts new file mode 100644 index 0000000000..0a3ed2b7f0 --- /dev/null +++ b/plugins/airbrake-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/airbrake-backend/src/service/router.test.ts b/plugins/airbrake-backend/src/service/router.test.ts new file mode 100644 index 0000000000..7ccafd7eba --- /dev/null +++ b/plugins/airbrake-backend/src/service/router.test.ts @@ -0,0 +1,92 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { ConfigReader } from '@backstage/config'; +import { + createRouter, + generateAirbrakePathRewrite, + RouterOptions, +} from './router'; +import { AirbrakeConfig, extractAirbrakeConfig } from '../config'; +import * as winston from 'winston'; + +describe('createRouter', () => { + let app: express.Express; + let airbrakeConfig: AirbrakeConfig; + let voidLogger: winston.Logger; + + beforeEach(async () => { + jest.resetAllMocks(); + + voidLogger = getVoidLogger(); + const config = new ConfigReader({ + airbrake: { + apiKey: 'fakeApiKey', + }, + }); + airbrakeConfig = extractAirbrakeConfig(config); + + const router = await createRouter({ + logger: voidLogger, + airbrakeConfig, + }); + app = express().use(router); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); + + describe('GET /api', () => { + it('appends the API Key properly with no other url parameters', () => { + const options: RouterOptions = { + logger: voidLogger, + airbrakeConfig, + }; + const pathRewrite = generateAirbrakePathRewrite(options) as ( + path: string, + ) => string; + + expect(pathRewrite('/airbrake-backend/api/v4/random/endpoint')).toBe( + '/v4/random/endpoint?key=fakeApiKey', + ); + }); + + it('appends the API Key properly despite there being other URL parameters', () => { + const options: RouterOptions = { + logger: voidLogger, + airbrakeConfig, + }; + const pathRewrite = generateAirbrakePathRewrite(options) as ( + path: string, + ) => string; + + expect( + pathRewrite( + '/airbrake-backend/api/v4/random/endpoint?param1=123¶m2=abc', + ), + ).toBe('/v4/random/endpoint?param1=123¶m2=abc&key=fakeApiKey'); + }); + }); +}); diff --git a/plugins/airbrake-backend/src/service/router.ts b/plugins/airbrake-backend/src/service/router.ts new file mode 100644 index 0000000000..b870eb5c13 --- /dev/null +++ b/plugins/airbrake-backend/src/service/router.ts @@ -0,0 +1,96 @@ +/* + * 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 { errorHandler } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { createProxyMiddleware } from 'http-proxy-middleware'; +import { AirbrakeConfig } from '../config'; +import { Options } from 'http-proxy-middleware/dist/types'; + +/** + * The router options that are needed when creating a router. + * + * @public + */ +export interface RouterOptions { + /** + * A logger object + */ + logger: Logger; + + /** + * The Airbrake config obtained from {@link extractAirbrakeConfig} + */ + airbrakeConfig: AirbrakeConfig; +} + +/** + * Mainly used internally to generate the path. + * + * @internal + * + * @param options - Router options + */ +export const generateAirbrakePathRewrite = ( + options: RouterOptions, +): Options['pathRewrite'] => { + const apiKey = options.airbrakeConfig.apiKey; + + return path => { + let newPath = path.replace(/.+?(\/api)/g, ''); + if (newPath.includes('?')) { + newPath += `&key=${apiKey}`; + } else { + newPath += `?key=${apiKey}`; + } + return newPath; + }; +}; + +/** + * Create the Airbrake Router, used for making API calls to the Airbrake API. + * + * @public + * + * @param options - Router options + */ +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.send({ status: 'ok' }); + }); + + router.use( + '/api', + createProxyMiddleware({ + target: 'https://api.airbrake.io/api', + changeOrigin: true, + pathRewrite: generateAirbrakePathRewrite(options), + }), + ); + + router.use(errorHandler()); + return router; +} diff --git a/plugins/airbrake-backend/src/service/standaloneServer.ts b/plugins/airbrake-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..6825e60a0a --- /dev/null +++ b/plugins/airbrake-backend/src/service/standaloneServer.ts @@ -0,0 +1,61 @@ +/* + * 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 { + createServiceBuilder, + loadBackendConfig, +} from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; +import { extractAirbrakeConfig } from '../config'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'airbrake-backend' }); + const config = await loadBackendConfig({ logger, argv: process.argv }); + const airbrakeConfig = extractAirbrakeConfig(config); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + airbrakeConfig, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/api/airbrake', router); + if (options.enableCors) { + logger.info('CORS is enabled, limiting to localhost with port 3000'); + service = service.enableCors({ origin: 'http://localhost:3000' }); + } else { + logger.info('CORS is disabled, allowing all origins'); + service = service.enableCors({ origin: '*' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/index.ts b/plugins/airbrake-backend/src/setupTests.ts similarity index 92% rename from packages/core-app-api/src/apis/implementations/auth/auth0/index.ts rename to plugins/airbrake-backend/src/setupTests.ts index 9daed7d13e..d3232290a7 100644 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/index.ts +++ b/plugins/airbrake-backend/src/setupTests.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default as Auth0Auth } from './Auth0Auth'; +export {}; diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index df784d0030..685ef4c0e7 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-airbrake +## 0.3.0 + +### Minor Changes + +- da78e79a94: This marks the first release where the Airbrake plugin is useable. Airbrake frontend and Airbrake backend work with each other. Currently just a summary of the latest Airbrakes is shown on Backstage. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/dev-utils@0.2.23 + - @backstage/test-utils@0.2.6 + +## 0.2.0 + +### Minor Changes + +- 9e505d20a3: API connectivity has added, but currently will only work by running it standalone on a browser with CORS disabled. + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/test-utils@0.2.5 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/dev-utils@0.2.22 + - @backstage/theme@0.2.15 + ## 0.1.3 ### Patch Changes diff --git a/plugins/airbrake/README.md b/plugins/airbrake/README.md index de041fb3ec..bc830c326d 100644 --- a/plugins/airbrake/README.md +++ b/plugins/airbrake/README.md @@ -1,13 +1,113 @@ # Airbrake -Welcome to the Airbrake plugin! +The Airbrake plugin provides connectivity between Backstage and Airbrake (https://airbrake.io/). -This is a plugin providing connectivity between Backstage and Airbrake (https://airbrake.io/). +## How to use -_This plugin is currently not fit for use as it is work in progress_ +1. Install the Frontend plugin: -## Getting started + ```bash + # From your Backstage root directory + cd packages/app + yarn add @backstage/plugin-airbrake + ``` -You can serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. +2. Install the Backend plugin: + + ```bash + # From your Backstage root directory + cd packages/backend + yarn add @backstage/plugin-airbrake-backend + ``` + +3. Add the `EntityAirbrakeContent` to `packages/app/src/components/catalog/EntityPage.tsx` for all the entity pages you want Airbrake to be in: + + ```typescript jsx + import { EntityAirbrakeContent } from '@backstage/plugin-airbrake'; + + const serviceEntityPage = ( + + + + + + ); + + const websiteEntityPage = ( + + + + + + ); + + const defaultEntityPage = ( + + + + + + ); + ``` + +4. Create `packages/backend/src/plugins/airbrake.ts` with these contents: + + ```typescript + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + import { + createRouter, + extractAirbrakeConfig, + } from '@backstage/plugin-airbrake-backend'; + + export default async function createPlugin({ + logger, + config, + }: PluginEnvironment): Promise { + return createRouter({ + logger, + airbrakeConfig: extractAirbrakeConfig(config), + }); + } + ``` + +5. Setup the Backend code in `packages/backend/src/index.ts`: + + ```typescript + import airbrake from './plugins/airbrake'; + + async function main() { + //... After const createEnv = makeCreateEnv(config) ... + + const airbrakeEnv = useHotMemoize(module, () => createEnv('airbrake')); + + //... After const apiRouter = Router() ... + apiRouter.use('/airbrake', await airbrake(airbrakeEnv)); + } + ``` + +6. Add this config as a top level section in your `app-config.yaml`: + + ```yaml + airbrake: + apiKey: ${AIRBRAKE_API_KEY} + ``` + +7. Set an environment variable `AIRBRAKE_API_KEY` with your [API key](https://airbrake.io/docs/api/#authentication) + before starting Backstage backend. + +8. Add the following annotation to the `catalog-info.yaml` for a repo you want to link to an Airbrake project: + + ```yaml + metadata: + annotations: + airbrake.io/project-id: '123456' + ``` + +## Local Development + +Start this plugin in standalone mode by running `yarn start` inside the plugin directory. This method of serving the plugin provides quicker +iteration speed and a faster startup and hot reloads. It is only meant for local development, and the setup for it can +be found inside the [/dev](./dev) directory. + +> A mock API will be used to run it in standalone. If you want to talk to the real API [follow the instructions to start up Airbrake Backend in standalone](../airbrake-backend/README.md#local-development). diff --git a/plugins/airbrake/api-report.md b/plugins/airbrake/api-report.md index c01aa938ae..bd274e604d 100644 --- a/plugins/airbrake/api-report.md +++ b/plugins/airbrake/api-report.md @@ -8,9 +8,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; -// Warning: (ae-missing-release-tag) "airbrakePlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const airbrakePlugin: BackstagePlugin< { root: RouteRef; @@ -18,10 +16,6 @@ export const airbrakePlugin: BackstagePlugin< {} >; -// Warning: (ae-missing-release-tag) "EntityAirbrakeContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const EntityAirbrakeContent: () => JSX.Element; - -// (No @packageDocumentation comment for this package) ``` diff --git a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx new file mode 100644 index 0000000000..7a4ee42f14 --- /dev/null +++ b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx @@ -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 React from 'react'; +import { + createTheme, + makeStyles, + MuiThemeProvider, + TextField, +} from '@material-ui/core'; +import { Context } from '../ContextProvider'; + +const useStyles = makeStyles({ + root: { + display: 'flex', + gap: '1em', + flexWrap: 'wrap', + }, +}); + +const textFieldTheme = createTheme({ + palette: { + type: 'dark', + primary: { + light: '#fff', + main: '#fff', + dark: '#fff', + contrastText: '#fff', + }, + secondary: { + light: '#fff', + main: '#fff', + dark: '#fff', + contrastText: '#fff', + }, + action: { + disabled: '#fff', + }, + text: { + primary: '#fff', + secondary: '#fff', + }, + }, +}); + +export const ApiBar = () => { + const classes = useStyles(); + + return ( + + {value => ( +

+ + + value.setProjectId?.(parseInt(e.target.value, 10) || undefined) + } + /> + +
+ )} + + ); +}; diff --git a/plugins/scaffolder/src/components/FavouriteTemplate/index.ts b/plugins/airbrake/dev/components/ApiBar/index.ts similarity index 94% rename from plugins/scaffolder/src/components/FavouriteTemplate/index.ts rename to plugins/airbrake/dev/components/ApiBar/index.ts index 5955e88c01..ff6e80d3d8 100644 --- a/plugins/scaffolder/src/components/FavouriteTemplate/index.ts +++ b/plugins/airbrake/dev/components/ApiBar/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './FavouriteTemplate'; +export { ApiBar } from './ApiBar'; diff --git a/plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx b/plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx new file mode 100644 index 0000000000..d46366d8f6 --- /dev/null +++ b/plugins/airbrake/dev/components/ContextProvider/ContextProvider.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, { + Dispatch, + PropsWithChildren, + SetStateAction, + useState, +} from 'react'; + +interface ContextInterface { + projectId?: number; + setProjectId?: Dispatch>; +} + +export const Context = React.createContext({}); + +export const ContextProvider = ({ children }: PropsWithChildren<{}>) => { + const [projectId, setProjectId] = useState(); + + return ( + + {children} + + ); +}; diff --git a/plugins/scaffolder-backend/src/lib/catalog/index.ts b/plugins/airbrake/dev/components/ContextProvider/index.ts similarity index 90% rename from plugins/scaffolder-backend/src/lib/catalog/index.ts rename to plugins/airbrake/dev/components/ContextProvider/index.ts index ea693b00b3..f1e350868a 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/index.ts +++ b/plugins/airbrake/dev/components/ContextProvider/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { CatalogEntityClient } from './CatalogEntityClient'; + +export { ContextProvider, Context } from './ContextProvider'; diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index e74ce68ee5..a844dfdfdc 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -15,39 +15,74 @@ */ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { EntityAirbrakeContent, airbrakePlugin } from '../src/plugin'; +import { TestApiProvider } from '@backstage/test-utils'; +import { airbrakePlugin, EntityAirbrakeContent } from '../src'; import { - Content, - ContentHeader, - Header, - HeaderLabel, - Page, - SupportButton, -} from '@backstage/core-components'; + airbrakeApiRef, + localDiscoveryApi, + MockAirbrakeApi, + ProductionAirbrakeApi, +} from '../src/api'; +import { ApiBar } from './components/ApiBar'; +import { Content, Header, Page } from '@backstage/core-components'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { createEntity } from '../src/api'; +import CloudOffIcon from '@material-ui/icons/CloudOff'; +import CloudIcon from '@material-ui/icons/Cloud'; +import { Context, ContextProvider } from './components/ContextProvider'; createDevApp() .registerPlugin(airbrakePlugin) .addPage({ element: ( -
- - -
+
- - - A description of your plugin goes here. - - - + + + + + ), - title: 'Root Page', - path: '/airbrake', + title: 'Mock API', + path: '/airbrake-mock-api', + icon: CloudOffIcon, + }) + .addPage({ + element: ( + + +
+ +
+ + + {value => ( + + + + + + )} + + +
+
+ ), + title: 'Real API', + path: '/airbrake-real-api', + icon: CloudIcon, }) .render(); diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 40b4787dd5..7342fdd839 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.1.3", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,55 +9,52 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/dev-utils": "^0.2.23", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/test-utils": "^0.2.6", + "@backstage/theme": "^0.2.15", "@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", - "object-hash": "^2.2.0" + "object-hash": "^2.2.0", + "react-use": "^17.2.4" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@types/object-hash": "^2.2.1", - "@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", + "@backstage/app-defaults": "^0.1.9", + "@backstage/cli": "^0.14.1", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@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", + "@types/object-hash": "^2.2.1", "cross-fetch": "^3.1.5", + "msw": "^0.35.0", "react-router": "6.0.0-beta.0" }, "files": [ "dist" - ], - "jest": { - "coverageThreshold": { - "global": { - "functions": 100, - "lines": 100, - "statements": 100 - } - } - } + ] } diff --git a/plugins/airbrake/src/api/AirbrakeApi.ts b/plugins/airbrake/src/api/AirbrakeApi.ts new file mode 100644 index 0000000000..a29aa3eca7 --- /dev/null +++ b/plugins/airbrake/src/api/AirbrakeApi.ts @@ -0,0 +1,26 @@ +/* + * 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 { Groups } from './airbrakeGroups'; +import { createApiRef } from '@backstage/core-plugin-api'; + +export const airbrakeApiRef = createApiRef({ + id: 'plugin.airbrake.service', +}); + +export interface AirbrakeApi { + fetchGroups(projectId: string): Promise; +} diff --git a/plugins/airbrake/src/api/ProductionApi.test.ts b/plugins/airbrake/src/api/ProductionApi.test.ts new file mode 100644 index 0000000000..f09127b4c2 --- /dev/null +++ b/plugins/airbrake/src/api/ProductionApi.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProductionAirbrakeApi } from './ProductionApi'; +import { rest } from 'msw'; +import mockGroupsData from './mock/airbrakeGroupsApiMock.json'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { localDiscoveryApi } from './mock'; + +describe('The production Airbrake API', () => { + const productionApi = new ProductionAirbrakeApi(localDiscoveryApi); + const worker = setupServer(); + setupRequestMockHandlers(worker); + + it('fetches groups using the provided project ID', async () => { + worker.use( + rest.get( + 'http://localhost:7007/api/airbrake/api/v4/projects/123456/groups', + (_, res, ctx) => { + return res(ctx.status(200), ctx.json(mockGroupsData)); + }, + ), + ); + + const groups = await productionApi.fetchGroups('123456'); + + expect(groups).toStrictEqual(mockGroupsData); + }); + + it('throws if fetching groups was unsuccessful', async () => { + worker.use( + rest.get( + 'http://localhost:7007/api/airbrake/api/v4/projects/123456/groups', + (_, res, ctx) => { + return res(ctx.status(500)); + }, + ), + ); + + await expect(productionApi.fetchGroups('123456')).rejects.toThrow(); + }); +}); diff --git a/plugins/airbrake/src/api/ProductionApi.ts b/plugins/airbrake/src/api/ProductionApi.ts new file mode 100644 index 0000000000..21ca19ee7d --- /dev/null +++ b/plugins/airbrake/src/api/ProductionApi.ts @@ -0,0 +1,36 @@ +/* + * 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 { Groups } from './airbrakeGroups'; +import { AirbrakeApi } from './AirbrakeApi'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; + +export class ProductionAirbrakeApi implements AirbrakeApi { + constructor(private readonly discoveryApi: DiscoveryApi) {} + + async fetchGroups(projectId: string): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('airbrake'); + const apiUrl = `${baseUrl}/api/v4/projects/${projectId}/groups`; + + const response = await fetch(apiUrl); + + if (response.status >= 400 && response.status < 600) { + throw new Error('Failed fetching Airbrake groups'); + } + + return (await response.json()) as Groups; + } +} diff --git a/plugins/airbrake/src/api/airbrakeGroups.d.ts b/plugins/airbrake/src/api/airbrakeGroups.d.ts new file mode 100644 index 0000000000..e1887eca9b --- /dev/null +++ b/plugins/airbrake/src/api/airbrakeGroups.d.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Groups { + count: number; + end: string; + groups?: Group[] | null; + page: number; + resolvedCount: number; + unresolvedCount: number; +} + +export interface Group { + id: string; + projectId: number; + resolved: boolean; + muted: boolean; + mutedBy: number; + mutedAt?: string | null; + errors?: Error[]; + attributes?: string | null; + context: Context; + lastDeployId: string; + lastDeployAt?: string | null; + lastNoticeId: string; + lastNoticeAt: string; + noticeCount: number; + noticeTotalCount: number; + commentCount: number; + createdAt: string; +} + +export interface Error { + type: string; + message: string; + backtrace?: Backtrace[]; +} + +export interface Backtrace { + file: string; + function: string; + line: number; + column: number; + code?: string | null; +} + +export interface Context { + action: string; + component: string; + environment: string; + severity: string; +} diff --git a/plugins/airbrake/src/api/index.ts b/plugins/airbrake/src/api/index.ts new file mode 100644 index 0000000000..2d92789653 --- /dev/null +++ b/plugins/airbrake/src/api/index.ts @@ -0,0 +1,21 @@ +/* + * 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 * from './mock'; +export type { AirbrakeApi } from './AirbrakeApi'; +export { airbrakeApiRef } from './AirbrakeApi'; +export type { Groups } from './airbrakeGroups'; +export { ProductionAirbrakeApi } from './ProductionApi'; diff --git a/plugins/airbrake/src/api/mock/LocalDiscoveryApi.ts b/plugins/airbrake/src/api/mock/LocalDiscoveryApi.ts new file mode 100644 index 0000000000..c3080a8104 --- /dev/null +++ b/plugins/airbrake/src/api/mock/LocalDiscoveryApi.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DiscoveryApi } from '@backstage/core-plugin-api'; + +export const localDiscoveryApi: DiscoveryApi = { + async getBaseUrl(pluginId: string): Promise { + return `http://localhost:7007/api/${pluginId}`; + }, +}; diff --git a/plugins/airbrake/src/api/mock/MockApi.ts b/plugins/airbrake/src/api/mock/MockApi.ts new file mode 100644 index 0000000000..dbb1d5c049 --- /dev/null +++ b/plugins/airbrake/src/api/mock/MockApi.ts @@ -0,0 +1,33 @@ +/* + * 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 { Groups } from '../airbrakeGroups'; +import { AirbrakeApi } from '../AirbrakeApi'; +import mockGroupsData from './airbrakeGroupsApiMock.json'; + +export class MockAirbrakeApi implements AirbrakeApi { + waitTimeInMillis: number; + + constructor(waitTimeInMillis = 10) { + this.waitTimeInMillis = waitTimeInMillis; + } + + fetchGroups(): Promise { + return new Promise(resolve => { + setTimeout(() => resolve(mockGroupsData), this.waitTimeInMillis); + }); + } +} diff --git a/plugins/airbrake/src/api/mock/MockEntity.ts b/plugins/airbrake/src/api/mock/MockEntity.ts new file mode 100644 index 0000000000..13534f2458 --- /dev/null +++ b/plugins/airbrake/src/api/mock/MockEntity.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AIRBRAKE_PROJECT_ID_ANNOTATION } from '../../components/useProjectId'; +import { Entity } from '@backstage/catalog-model'; + +export const createEntity = (projectId?: number) => { + const projectIdString = projectId?.toString(); + + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [AIRBRAKE_PROJECT_ID_ANNOTATION]: projectIdString, + }, + name: projectIdString, + }, + } as Entity; +}; diff --git a/plugins/airbrake/src/components/EntityAirbrakeContent/example-data.json b/plugins/airbrake/src/api/mock/airbrakeGroupsApiMock.json similarity index 100% rename from plugins/airbrake/src/components/EntityAirbrakeContent/example-data.json rename to plugins/airbrake/src/api/mock/airbrakeGroupsApiMock.json diff --git a/plugins/airbrake/src/api/mock/index.ts b/plugins/airbrake/src/api/mock/index.ts new file mode 100644 index 0000000000..4e0ab5fabf --- /dev/null +++ b/plugins/airbrake/src/api/mock/index.ts @@ -0,0 +1,19 @@ +/* + * 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 * from './MockApi'; +export * from './MockEntity'; +export * from './LocalDiscoveryApi'; diff --git a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.test.tsx deleted file mode 100644 index a621eb281c..0000000000 --- a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.test.tsx +++ /dev/null @@ -1,32 +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 React from 'react'; -import { EntityAirbrakeContent } from './EntityAirbrakeContent'; -import exampleData from './example-data.json'; -import { renderInTestApp } from '@backstage/test-utils'; - -describe('EntityAirbrakeContent', () => { - it('renders all errors sent from Airbrake', async () => { - const table = await renderInTestApp(); - expect(exampleData.groups.length).toBeGreaterThan(0); - for (const group of exampleData.groups) { - expect( - await table.getByText(group.errors[0].message), - ).toBeInTheDocument(); - } - }); -}); diff --git a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx b/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx deleted file mode 100644 index 062d9e898a..0000000000 --- a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { Grid, Typography } from '@material-ui/core'; -import { InfoCard } from '@backstage/core-components'; -import exampleData from './example-data.json'; -import hash from 'object-hash'; -import { makeStyles } from '@material-ui/core/styles'; -import { BackstageTheme } from '@backstage/theme'; - -const useStyles = makeStyles(() => ({ - multilineText: { - whiteSpace: 'pre-wrap', - }, -})); - -export const EntityAirbrakeContent = () => { - const classes = useStyles(); - - return ( - - {exampleData.groups.map(group => ( - - {group.errors.map(error => ( - - - {error.message} - - - ))} - - ))} - - ); -}; diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx new file mode 100644 index 0000000000..ee14a17f9a --- /dev/null +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -0,0 +1,96 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; +import exampleData from '../../api/mock/airbrakeGroupsApiMock.json'; +import { + MockErrorApi, + renderInTestApp, + setupRequestMockHandlers, + TestApiProvider, +} from '@backstage/test-utils'; +import { createEntity } from '../../api'; +import { + airbrakeApiRef, + localDiscoveryApi, + MockAirbrakeApi, + ProductionAirbrakeApi, +} from '../../api'; +import { errorApiRef } from '@backstage/core-plugin-api'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +describe('EntityAirbrakeContent', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + it('renders all errors sent from Airbrake', async () => { + const widget = await renderInTestApp( + + + , + ); + expect(exampleData.groups.length).toBeGreaterThan(0); + for (const group of exampleData.groups) { + expect( + await widget.getByText(group.errors[0].message), + ).toBeInTheDocument(); + } + }); + + it('states that the annotation is missing if no project ID annotation is provided', async () => { + const widget = await renderInTestApp( + + + , + ); + await expect( + widget.findByText('Missing Annotation'), + ).resolves.toBeInTheDocument(); + }); + + it('states that an error occurred if the API call fails', async () => { + worker.use( + rest.get( + 'http://localhost:7007/api/airbrake/api/v4/projects/123/groups', + (_, res, ctx) => { + return res(ctx.status(500)); + }, + ), + ); + const mockErrorApi = new MockErrorApi({ collect: true }); + + const widget = await renderInTestApp( + + + , + ); + + await expect( + widget.findByText(/.*there was an issue communicating with Airbrake.*/), + ).resolves.toBeInTheDocument(); + expect(mockErrorApi.getErrors().length).toBe(1); + expect(mockErrorApi.getErrors()[0].error.message).toStrictEqual( + 'Failed fetching Airbrake groups', + ); + }); +}); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx new file mode 100644 index 0000000000..b11116aeef --- /dev/null +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -0,0 +1,94 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import React, { useEffect } from 'react'; +import { Grid, Typography } from '@material-ui/core'; +import { + EmptyState, + InfoCard, + MissingAnnotationEmptyState, + Progress, +} from '@backstage/core-components'; +import hash from 'object-hash'; +import { makeStyles } from '@material-ui/core/styles'; +import { BackstageTheme } from '@backstage/theme'; +import { ErrorApi, errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { airbrakeApiRef } from '../../api'; +import useAsync from 'react-use/lib/useAsync'; +import { AIRBRAKE_PROJECT_ID_ANNOTATION, useProjectId } from '../useProjectId'; + +const useStyles = makeStyles(() => ({ + multilineText: { + whiteSpace: 'pre-wrap', + }, +})); + +export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { + const classes = useStyles(); + + const projectId = useProjectId(entity); + const errorApi = useApi(errorApiRef); + const airbrakeApi = useApi(airbrakeApiRef); + + const { loading, value, error } = useAsync( + () => airbrakeApi.fetchGroups(projectId), + [airbrakeApi, projectId], + ); + + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + if (loading || !projectId || error) { + return ( + + {loading && } + + {!loading && !projectId && ( + + )} + + {!loading && error && ( + + )} + + ); + } + + return ( + + {value?.groups?.map(group => ( + + {group.errors?.map(groupError => ( + + + {groupError.message} + + + ))} + + ))} + + ); +}; diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/index.ts b/plugins/airbrake/src/components/EntityAirbrakeWidget/index.ts new file mode 100644 index 0000000000..0a85263c5f --- /dev/null +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; diff --git a/plugins/airbrake/src/components/useProjectId.ts b/plugins/airbrake/src/components/useProjectId.ts new file mode 100644 index 0000000000..3402778b7b --- /dev/null +++ b/plugins/airbrake/src/components/useProjectId.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; + +export const AIRBRAKE_PROJECT_ID_ANNOTATION = 'airbrake.io/project-id'; + +export const useProjectId = (entity: Entity) => { + return entity?.metadata.annotations?.[AIRBRAKE_PROJECT_ID_ANNOTATION] ?? ''; +}; diff --git a/plugins/airbrake/src/extensions.test.tsx b/plugins/airbrake/src/extensions.test.tsx new file mode 100644 index 0000000000..05afe8f1e3 --- /dev/null +++ b/plugins/airbrake/src/extensions.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 { EntityAirbrakeContent } from './extensions'; +import { Route } from 'react-router'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { airbrakeApiRef, MockAirbrakeApi } from './api'; +import { createEntity } from './api'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; + +describe('The Airbrake entity', () => { + it('should render the content properly', async () => { + const rendered = await renderInTestApp( + + + } /> + + , + ); + await expect( + rendered.findByText('ChunkLoadError'), + ).resolves.toBeInTheDocument(); + }); +}); diff --git a/plugins/airbrake/src/extensions.tsx b/plugins/airbrake/src/extensions.tsx new file mode 100644 index 0000000000..d4f890f44a --- /dev/null +++ b/plugins/airbrake/src/extensions.tsx @@ -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 { useEntity } from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { airbrakePlugin } from './plugin'; +import { createRoutableExtension } from '@backstage/core-plugin-api'; +import { rootRouteRef } from './routes'; + +/** + * This is the widget that shows up on a component page + * + * @public + */ +export const EntityAirbrakeContent = airbrakePlugin.provide( + createRoutableExtension({ + name: 'EntityAirbrakeContent', + mountPoint: rootRouteRef, + component: () => + import('./components/EntityAirbrakeWidget').then( + ({ EntityAirbrakeWidget }) => { + return () => { + const { entity } = useEntity(); + return ; + }; + }, + ), + }), +); diff --git a/plugins/airbrake/src/index.ts b/plugins/airbrake/src/index.ts index 8f319e02fd..2b2406776b 100644 --- a/plugins/airbrake/src/index.ts +++ b/plugins/airbrake/src/index.ts @@ -13,4 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { airbrakePlugin, EntityAirbrakeContent } from './plugin'; + +/** + * The Airbrake plugin provides connectivity between Backstage and Airbrake (https://airbrake.io/). + * + * @packageDocumentation + */ + +export { airbrakePlugin } from './plugin'; +export { EntityAirbrakeContent } from './extensions'; diff --git a/plugins/airbrake/src/plugin.test.ts b/plugins/airbrake/src/plugin.test.ts index c8e9c30e83..ad7865713f 100644 --- a/plugins/airbrake/src/plugin.test.ts +++ b/plugins/airbrake/src/plugin.test.ts @@ -15,9 +15,16 @@ */ import { airbrakePlugin } from './plugin'; +import { ProductionAirbrakeApi } from './api'; describe('catalog', () => { it('should export plugin', () => { expect(airbrakePlugin).toBeDefined(); }); + + it('should have at least one API, the production API', () => { + const apiFactories = Array.from(airbrakePlugin.getApis()); + expect(apiFactories.length).toBe(1); + expect(apiFactories[0].factory({})).toBeInstanceOf(ProductionAirbrakeApi); + }); }); diff --git a/plugins/airbrake/src/plugin.ts b/plugins/airbrake/src/plugin.ts index 0a3679baa4..56f65ba110 100644 --- a/plugins/airbrake/src/plugin.ts +++ b/plugins/airbrake/src/plugin.ts @@ -14,26 +14,29 @@ * limitations under the License. */ import { + createApiFactory, createPlugin, - createRoutableExtension, + discoveryApiRef, } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; +import { airbrakeApiRef, ProductionAirbrakeApi } from './api'; +/** + * The Airbrake plugin instance + * + * @public + */ export const airbrakePlugin = createPlugin({ id: 'airbrake', + apis: [ + createApiFactory({ + api: airbrakeApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new ProductionAirbrakeApi(discoveryApi), + }), + ], routes: { root: rootRouteRef, }, }); - -export const EntityAirbrakeContent = airbrakePlugin.provide( - createRoutableExtension({ - name: 'EntityAirbrakeContent', - component: () => - import('./components/EntityAirbrakeContent/EntityAirbrakeContent').then( - m => m.EntityAirbrakeContent, - ), - mountPoint: rootRouteRef, - }), -); diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index 9f5d2cc730..43e83e2d1e 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-allure +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.15 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.1.14 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 5ea3129746..eaa3013da8 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.14", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,22 +11,25 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -37,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx index 34d12aa032..e6355d861f 100644 --- a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx @@ -23,6 +23,7 @@ import { setupRequestMockHandlers, renderInTestApp, } from '@backstage/test-utils'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; describe('ExampleComponent', () => { const server = setupServer(); @@ -39,7 +40,15 @@ describe('ExampleComponent', () => { it('should render', async () => { const rendered = await renderInTestApp( - + + + , ); expect(rendered.getByText('Missing Annotation')).toBeInTheDocument(); diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index e24ea89a0a..ccf5ebd98c 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-analytics-module-ga +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.10 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + ## 0.1.9 ### Patch Changes diff --git a/plugins/analytics-module-ga/README.md b/plugins/analytics-module-ga/README.md index 92cfd7e9ae..4b822ea3e3 100644 --- a/plugins/analytics-module-ga/README.md +++ b/plugins/analytics-module-ga/README.md @@ -47,6 +47,19 @@ app: trackingId: UA-0000000-0 ``` +4. Update CSP in your `app-config.yaml`: + +The following is the minimal content security policy required to load scripts from GA. + +```yaml +backend: + csp: + connect-src: ["'self'", 'http:', 'https:'] + # Add these two lines below + script-src: ["'self'", "'unsafe-eval'", 'https://www.google-analytics.com'] + img-src: ["'self'", 'data:', 'https://www.google-analytics.com'] +``` + ## Configuration In order to be able to analyze usage of your Backstage instance _by plugin_, we diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 0b0c3a6cdc..d71b66cd6f 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.9", + "version": "0.1.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,21 +10,24 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin-module" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,10 +38,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index 03bef4543a..e86a6cef5d 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-apache-airflow +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.7 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + ## 0.1.6 ### Patch Changes diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index e36e925fcb..b5045551bb 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.6", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,19 +9,22 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -33,10 +36,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@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 73de279056..6bbbe9d056 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-api-docs +## 0.8.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- 306c366fc9: chore(deps): bump `@asyncapi/react-component` +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/plugin-catalog@0.9.0 + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.7.3 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog@0.8.0 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.7.2 ### Patch Changes diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index b626da89a5..b6683d9b40 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -9,7 +9,6 @@ import { ApiEntity } from '@backstage/catalog-model'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogTableRow } from '@backstage/plugin-catalog'; -import { Entity } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -17,11 +16,10 @@ import { TableColumn } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ApiDefinitionCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ApiDefinitionCard: (_: Props) => JSX.Element; +export const ApiDefinitionCard: () => JSX.Element; // Warning: (ae-missing-release-tag) "ApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -92,13 +90,13 @@ export type AsyncApiDefinitionWidgetProps = { // Warning: (ae-missing-release-tag) "ConsumedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ConsumedApisCard: ({ variant }: Props_2) => JSX.Element; +export const ConsumedApisCard: ({ variant }: 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) "ConsumingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ConsumingComponentsCard: ({ variant }: Props_5) => JSX.Element; +export const ConsumingComponentsCard: ({ variant }: Props_4) => JSX.Element; // @public export const DefaultApiExplorerPage: ({ @@ -122,9 +120,7 @@ export function defaultDefinitionWidgets(): ApiDefinitionWidget[]; // Warning: (ae-missing-release-tag) "EntityApiDefinitionCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityApiDefinitionCard: (_: { - apiEntity?: ApiEntity | undefined; -}) => JSX.Element; +export const EntityApiDefinitionCard: () => JSX.Element; // Warning: (ae-missing-release-tag) "EntityConsumedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -132,7 +128,6 @@ export const EntityApiDefinitionCard: (_: { export const EntityConsumedApisCard: ({ variant, }: { - entity?: Entity | undefined; variant?: 'gridItem' | undefined; }) => JSX.Element; @@ -142,7 +137,6 @@ export const EntityConsumedApisCard: ({ export const EntityConsumingComponentsCard: ({ variant, }: { - entity?: Entity | undefined; variant?: 'gridItem' | undefined; }) => JSX.Element; @@ -161,7 +155,6 @@ export const EntityHasApisCard: ({ export const EntityProvidedApisCard: ({ variant, }: { - entity?: Entity | undefined; variant?: 'gridItem' | undefined; }) => JSX.Element; @@ -171,7 +164,6 @@ export const EntityProvidedApisCard: ({ export const EntityProvidingComponentsCard: ({ variant, }: { - entity?: Entity | undefined; variant?: 'gridItem' | undefined; }) => JSX.Element; @@ -193,7 +185,7 @@ export type GraphQlDefinitionWidgetProps = { // Warning: (ae-missing-release-tag) "HasApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const HasApisCard: ({ variant }: Props_3) => JSX.Element; +export const HasApisCard: ({ variant }: Props_2) => JSX.Element; // Warning: (ae-missing-release-tag) "OpenApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -228,11 +220,11 @@ export type PlainApiDefinitionWidgetProps = { // Warning: (ae-missing-release-tag) "ProvidedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ProvidedApisCard: ({ variant }: Props_4) => JSX.Element; +export const ProvidedApisCard: ({ 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) "ProvidingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ProvidingComponentsCard: ({ variant }: Props_6) => JSX.Element; +export const ProvidingComponentsCard: ({ variant }: Props_5) => JSX.Element; ``` diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 3b92cdf17e..331845bd13 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.2", + "version": "0.8.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -20,23 +23,23 @@ "backstage" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@asyncapi/react-component": "1.0.0-next.32", - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog": "^0.7.12", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@asyncapi/react-component": "1.0.0-next.33", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog": "^0.9.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -53,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@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/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx index 6f3e155816..353b973626 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx @@ -24,12 +24,7 @@ import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget'; import { CardTab, TabbedCard } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - apiEntity?: ApiEntity; -}; - -export const ApiDefinitionCard = (_: Props) => { +export const ApiDefinitionCard = () => { const { entity } = useEntity(); const config = useApi(apiDocsConfigRef); const { getApiDefinitionWidget } = config; diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx index 9e4c861e24..5924af3ef3 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -22,11 +22,13 @@ import { configApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { CatalogTableRow } from '@backstage/plugin-catalog'; +import { + CatalogTableRow, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog'; import { CatalogApi, catalogApiRef, - DefaultStarredEntitiesApi, entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; @@ -56,8 +58,8 @@ describe('DefaultApiExplorerPage', () => { }, ] as Entity[], }), - getLocationByEntity: () => - Promise.resolve({ id: 'id', type: 'github', target: 'url' }), + getLocationByRef: () => + Promise.resolve({ id: 'id', type: 'url', target: 'url' }), getEntityByName: async entityName => { return { apiVersion: 'backstage.io/v1alpha1', @@ -66,7 +68,8 @@ describe('DefaultApiExplorerPage', () => { relations: [ { type: RELATION_MEMBER_OF, - target: { namespace: 'default', kind: 'Group', name: 'tools' }, + targetRef: 'group:default/tools', + target: { namespace: 'default', kind: 'group', name: 'tools' }, }, ], }; diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx index 5e7b22524a..f84f66cee8 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -36,7 +36,7 @@ describe('', () => { getEntityByName: jest.fn(), getEntities: jest.fn(), addLocation: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; let Wrapper: React.ComponentType; @@ -95,10 +95,11 @@ describe('', () => { relations: [ { target: { - kind: 'API', + kind: 'api', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'api:my-namespace/target-name', type: RELATION_CONSUMES_API, }, ], diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx index 56b8559537..e810e06ff7 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - ApiEntity, - Entity, - RELATION_CONSUMES_API, -} from '@backstage/catalog-model'; +import { ApiEntity, RELATION_CONSUMES_API } from '@backstage/catalog-model'; import { Typography } from '@material-ui/core'; import { EntityTable, @@ -36,8 +32,6 @@ import { } from '@backstage/core-components'; type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; variant?: 'gridItem'; }; diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx index 4102bebf0e..daa909c7f8 100644 --- a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx @@ -36,7 +36,7 @@ describe('', () => { getEntityByName: jest.fn(), getEntities: jest.fn(), addLocation: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; let Wrapper: React.ComponentType; @@ -95,10 +95,11 @@ describe('', () => { relations: [ { target: { - kind: 'API', + kind: 'api', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'api:my-namespace/target-name', type: RELATION_HAS_PART, }, ], diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx index b29075aee9..852386140c 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -36,7 +36,7 @@ describe('', () => { getEntityByName: jest.fn(), getEntities: jest.fn(), addLocation: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; let Wrapper: React.ComponentType; @@ -95,10 +95,11 @@ describe('', () => { relations: [ { target: { - kind: 'API', + kind: 'api', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'api:my-namespace/target-name', type: RELATION_PROVIDES_API, }, ], diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx index efdc88b2e1..a63546176f 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - ApiEntity, - Entity, - RELATION_PROVIDES_API, -} from '@backstage/catalog-model'; +import { ApiEntity, RELATION_PROVIDES_API } from '@backstage/catalog-model'; import { Typography } from '@material-ui/core'; import { EntityTable, @@ -36,8 +32,6 @@ import { } from '@backstage/core-components'; type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; variant?: 'gridItem'; }; diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx index 06022eec69..a62b2119c7 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx @@ -32,7 +32,7 @@ describe('', () => { getEntityByName: jest.fn(), getEntities: jest.fn(), addLocation: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; let Wrapper: React.ComponentType; @@ -98,10 +98,11 @@ describe('', () => { relations: [ { target: { - kind: 'Component', + kind: 'component', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'component:my-namespace/target-name', type: RELATION_API_CONSUMED_BY, }, ], diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx index 20fa7021cc..5455f37f14 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx @@ -16,7 +16,6 @@ import { ComponentEntity, - Entity, RELATION_API_CONSUMED_BY, } from '@backstage/catalog-model'; import { Typography } from '@material-ui/core'; @@ -35,8 +34,6 @@ import { } from '@backstage/core-components'; type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; variant?: 'gridItem'; }; diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx index 275338d2f9..1a0aae12c7 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx @@ -32,7 +32,7 @@ describe('', () => { getEntityByName: jest.fn(), getEntities: jest.fn(), addLocation: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; let Wrapper: React.ComponentType; @@ -98,10 +98,11 @@ describe('', () => { relations: [ { target: { - kind: 'Component', + kind: 'component', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'component:my-namespace/target-name', type: RELATION_API_PROVIDED_BY, }, ], diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx index 286a360e9a..9bcace393e 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx @@ -16,7 +16,6 @@ import { ComponentEntity, - Entity, RELATION_API_PROVIDED_BY, } from '@backstage/catalog-model'; import { Typography } from '@material-ui/core'; @@ -35,8 +34,6 @@ import { } from '@backstage/core-components'; type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; variant?: 'gridItem'; }; diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 882979326e..baf9a928d8 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-app-backend +## 0.3.27 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/config-loader@0.9.6 + +## 0.3.26 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/config-loader@0.9.5 + - @backstage/types@0.1.3 + +## 0.3.25 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 0107c9aa08: chore(deps): bump `helmet` from 4.4.1 to 5.0.2 +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/config-loader@0.9.4 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.3.24 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 52da00fed3..79682ca9de 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.24", + "version": "0.3.27", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,25 +24,25 @@ "backstage" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config-loader": "^0.9.3", - "@backstage/config": "^0.1.13", - "@backstage/types": "^0.1.1", + "@backstage/backend-common": "^0.11.0", + "@backstage/config-loader": "^0.9.6", + "@backstage/config": "^0.1.15", + "@backstage/types": "^0.1.3", "@types/express": "^4.17.6", "globby": "^11.0.0", "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", - "helmet": "^4.0.0", + "helmet": "^5.0.2", "knex": "^1.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", @@ -47,9 +50,9 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.17", - "@backstage/cli": "^0.13.2", - "@backstage/types": "^0.1.1", + "@backstage/backend-test-utils": "^0.1.19", + "@backstage/cli": "^0.14.1", + "@backstage/types": "^0.1.3", "@types/supertest": "^2.0.8", "mock-fs": "^5.1.0", "msw": "^0.35.0", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index a19153c4e9..5f85363fcc 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-auth-backend +## 0.11.0 + +### Minor Changes + +- 3884bf0348: **BREAKING**: The default sign-in resolvers for all providers, if you choose to + use them, now emit the token `sub` and `ent` claims on the standard, + all-lowercase form, instead of the mixed-case form. The mixed-case form causes + problems for implementations that naively do string comparisons on refs. The end + result is that you may for example see your Backstage token `sub` claim now + become `'user:default/my-id'` instead of `'user:default/My-ID'`. + + On a related note, specifically the SAML provider now correctly issues both + `sub` and `ent` claims, and on the full entity ref form instead of the short + form with only the ID. + + **NOTE**: For a long time, it has been strongly recommended that you provide + your own sign-in resolver instead of using the builtin ones, and that will + become mandatory in the future. + +### Patch Changes + +- d64b8d3678: chore(deps): bump `minimatch` from 3.0.4 to 5.0.0 +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/plugin-auth-node@0.1.3 + +## 0.10.2 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-client@0.7.1 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/types@0.1.3 + - @backstage/plugin-auth-node@0.1.2 + +## 0.10.1 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- a31559d1f5: Bump `passport-oauth2` to version 1.6.1 +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- 1433045c08: Removed unused `helmet` dependency. +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/catalog-client@0.7.0 + - @backstage/errors@0.2.1 + - @backstage/plugin-auth-node@0.1.1 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.10.0 ### Minor Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 5d40523895..7a370ee8a9 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.10.0", + "version": "0.11.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,22 +24,22 @@ "backstage" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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", - "@backstage/types": "^0.1.1", + "@backstage/plugin-auth-node": "^0.1.3", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/types": "^0.1.3", "@google-cloud/firestore": "^5.0.2", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", @@ -48,13 +51,12 @@ "express-session": "^1.17.1", "fs-extra": "9.1.0", "google-auth-library": "^7.6.1", - "helmet": "^4.0.0", "jose": "^1.27.1", "jwt-decode": "^3.1.0", "knex": "^1.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", - "minimatch": "^3.0.3", + "minimatch": "^5.0.0", "morgan": "^1.10.0", "node-fetch": "^2.6.7", "node-cache": "^5.1.2", @@ -74,8 +76,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@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/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 897233294f..d2cee7bd89 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -30,11 +30,11 @@ describe('CatalogIdentityClient', () => { getEntities: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; const tokenManager: jest.Mocked = { getToken: jest.fn(), @@ -79,8 +79,9 @@ describe('CatalogIdentityClient', () => { relations: [ { type: RELATION_MEMBER_OF, + targetRef: 'group:default/team-a', target: { - kind: 'Group', + kind: 'group', namespace: 'default', name: 'team-a', }, @@ -100,8 +101,9 @@ describe('CatalogIdentityClient', () => { relations: [ { type: RELATION_MEMBER_OF, + targetRef: 'group:reality/screen-actors-guild', target: { - kind: 'Group', + kind: 'group', namespace: 'reality', name: 'screen-actors-guild', }, diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index aecf018975..d948af2e05 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -120,11 +120,11 @@ export class CatalogIdentityClient { e => e!.relations ?.filter(r => r.type === RELATION_MEMBER_OF) - .map(r => r.target) ?? [], + .map(r => r.targetRef) ?? [], ); const newEntityRefs = [ - ...new Set(resolvedEntityRefs.concat(memberOf).map(stringifyEntityRef)), + ...new Set(resolvedEntityRefs.map(stringifyEntityRef).concat(memberOf)), ]; logger?.debug(`Found catalog membership: ${newEntityRefs.join()}`); diff --git a/plugins/auth-backend/src/lib/catalog/helpers.ts b/plugins/auth-backend/src/lib/catalog/helpers.ts index 61dc428b3f..db9b38e2a6 100644 --- a/plugins/auth-backend/src/lib/catalog/helpers.ts +++ b/plugins/auth-backend/src/lib/catalog/helpers.ts @@ -27,11 +27,9 @@ export function getEntityClaims(entity: UserEntity): TokenParams['claims'] { const membershipRefs = entity.relations ?.filter( - r => - r.type === RELATION_MEMBER_OF && - r.target.kind.toLocaleLowerCase('en-US') === 'group', + r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'), ) - .map(r => stringifyEntityRef(r.target)) ?? []; + .map(r => r.targetRef) ?? []; return { sub: userRef, diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 75a3605483..07611ddb9c 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -18,7 +18,7 @@ import express, { CookieOptions } from 'express'; import crypto from 'crypto'; import { URL } from 'url'; import { - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; @@ -271,7 +271,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { const userEntityRef = stringifyEntityRef( parseEntityRef(identity.id, { defaultKind: 'user', - defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + defaultNamespace: DEFAULT_NAMESPACE, }), ); const token = await this.options.tokenIssuer.issueToken({ diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index ec7f8cafa6..29e8679fe0 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { Logger } from 'winston'; import { Profile as PassportProfile } from 'passport'; @@ -247,10 +251,16 @@ export const githubDefaultSignInResolver: SignInResolver< const userId = fullProfile.username || fullProfile.id; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ claims: { - sub: `user:default/${userId}`, - ent: [`user:default/${userId}`], + sub: entityRef, + ent: [entityRef], }, }); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 6689aba884..59f9a5a4cb 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -14,10 +14,13 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { Strategy as GitlabStrategy } from 'passport-gitlab2'; import { Logger } from 'winston'; - import { executeRedirectStrategy, executeFrameHandlerStrategy, @@ -71,8 +74,17 @@ export const gitlabDefaultSignInResolver: SignInResolver = async ( id = profile.email.split('@')[0]; } + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: `user:default/${id}`, ent: [`user:default/${id}`] }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id, token }; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index fbaf8c7462..e5e5698ec4 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; @@ -226,8 +230,17 @@ const googleDefaultSignInResolver: SignInResolver = async ( userId = profile.email.split('@')[0]; } + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id: userId, token }; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 25c940547b..98f94811bd 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as MicrosoftStrategy } from 'passport-microsoft'; @@ -231,10 +235,16 @@ export const microsoftDefaultSignInResolver: SignInResolver< const userId = profile.email.split('@')[0]; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ claims: { - sub: `user:default/${userId}`, - ent: [`user:default/${userId}`], + sub: entityRef, + ent: [entityRef], }, }); diff --git a/plugins/auth-backend/src/providers/oauth2/index.ts b/plugins/auth-backend/src/providers/oauth2/index.ts index 767466f8f6..d68208a148 100644 --- a/plugins/auth-backend/src/providers/oauth2/index.ts +++ b/plugins/auth-backend/src/providers/oauth2/index.ts @@ -15,5 +15,4 @@ */ export { createOAuth2Provider } from './provider'; - export type { OAuth2ProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 0906005791..d09337f5ad 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; @@ -210,8 +214,17 @@ export const oAuth2DefaultSignInResolver: SignInResolver = async ( const userId = profile.email.split('@')[0]; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id: userId, token }; diff --git a/plugins/auth-backend/src/providers/oidc/index.ts b/plugins/auth-backend/src/providers/oidc/index.ts index 39bd02928d..e63c9920df 100644 --- a/plugins/auth-backend/src/providers/oidc/index.ts +++ b/plugins/auth-backend/src/providers/oidc/index.ts @@ -13,5 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export type { OidcAuthResult, OidcProviderOptions } from './provider'; export { createOidcProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 9ccdf25f74..b4f34cd127 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { Client, @@ -211,21 +215,31 @@ export class OidcAuthProvider implements OAuthHandlers { } } -export const oAuth2DefaultSignInResolver: SignInResolver< - OidcAuthResult -> = async (info, ctx) => { +export const oidcDefaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { const { profile } = info; if (!profile.email) { throw new Error('Profile contained no email'); } + const userId = profile.email.split('@')[0]; + + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ claims: { - sub: `user:default/${userId}`, - ent: [`user:default/${userId}`], + sub: entityRef, + ent: [entityRef], }, }); + return { id: userId, token }; }; @@ -289,7 +303,7 @@ export const createOidcProvider = ( }, }); const signInResolverFn = - options?.signIn?.resolver ?? oAuth2DefaultSignInResolver; + options?.signIn?.resolver ?? oidcDefaultSignInResolver; const signInResolver: SignInResolver = info => signInResolverFn(info, { catalogIdentityClient, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 0ba7d241d3..26f586c578 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -13,6 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { OAuthAdapter, @@ -235,8 +240,17 @@ export const oktaDefaultSignInResolver: SignInResolver = async ( // TODO(Rugvip): Hardcoded to the local part of the email for now const userId = profile.email.split('@')[0]; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id: userId, token }; diff --git a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts index f3b234c08e..5a4e0895d9 100644 --- a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts +++ b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts @@ -15,7 +15,7 @@ */ import { - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; @@ -44,7 +44,7 @@ export function prepareBackstageIdentityResponse( const userEntityRef = stringifyEntityRef( parseEntityRef(sub, { defaultKind: 'user', - defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + defaultNamespace: DEFAULT_NAMESPACE, }), ); return { diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 1ca8cf0d5d..4458d423ef 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { SamlConfig } from 'passport-saml/lib/passport-saml/types'; import { @@ -150,8 +154,17 @@ const samlDefaultSignInResolver: SignInResolver = async ( ) => { const id = info.result.fullProfile.nameID; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id, token }; diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index a5103ea034..7676ce7afa 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-auth-node +## 0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + +## 0.1.2 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + +## 0.1.1 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + ## 0.1.0 ### Minor Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 4e059c5e19..2becee849d 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.1.0", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,25 +10,29 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "node-library" + }, "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", "jose": "^1.27.1", "node-fetch": "^2.6.7", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "msw": "^0.35.0", "uuid": "^8.0.0" }, diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index 0b44be4e13..0156f92891 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-azure-devops-backend +## 0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + +## 0.3.5 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/plugin-azure-devops-common@0.2.2 + +## 0.3.4 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/config@0.1.14 + - @backstage/plugin-azure-devops-common@0.2.1 + ## 0.3.3 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index a87a48046e..206294c863 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.3", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,19 +10,22 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config": "^0.1.13", - "@backstage/plugin-azure-devops-common": "^0.2.0", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/plugin-azure-devops-common": "^0.2.2", "@types/express": "^4.17.6", "azure-devops-node-api": "^11.0.1", "express": "^4.17.1", @@ -32,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", "msw": "^0.35.0" diff --git a/plugins/azure-devops-common/CHANGELOG.md b/plugins/azure-devops-common/CHANGELOG.md index 74129ea19e..65eaa862fb 100644 --- a/plugins/azure-devops-common/CHANGELOG.md +++ b/plugins/azure-devops-common/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-azure-devops-common +## 0.2.2 + +### Patch Changes + +- Fix for the previous release with missing type declarations. + +## 0.2.1 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` + ## 0.2.0 ### Minor Changes diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index 02faebca61..7810469bf9 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-common", - "version": "0.2.0", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,15 +24,15 @@ "backstage" ], "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0" + "@backstage/cli": "^0.14.0" }, "files": [ "dist" diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 8b0331e75d..7c0a533dd3 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-azure-devops +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.15 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + - @backstage/plugin-azure-devops-common@0.2.1 + ## 0.1.14 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index a34686d856..57c1654527 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.14", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -17,23 +20,23 @@ "directory": "plugins/azure-devops" }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@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.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-azure-devops-common": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -46,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index f025515703..ae93cdbe5a 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-badges-backend +## 0.1.21 + +### Patch Changes + +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + +## 0.1.20 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-client@0.7.1 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + +## 0.1.19 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/catalog-client@0.7.0 + - @backstage/errors@0.2.1 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + ## 0.1.18 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index b5fc38f289..94796ba665 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.18", + "version": "0.1.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,20 +25,20 @@ "badges" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/backend-common": "^0.11.0", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", "@types/express": "^4.17.6", "badge-maker": "^3.3.0", "cors": "^2.8.5", @@ -45,7 +48,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges-backend/src/badges.ts b/plugins/badges-backend/src/badges.ts index 9e4087e334..78108de080 100644 --- a/plugins/badges-backend/src/badges.ts +++ b/plugins/badges-backend/src/badges.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { InputError } from '@backstage/errors'; import { Badge, BadgeContext, BadgeFactories } from './types'; @@ -24,9 +24,9 @@ function appTitle(context: BadgeContext): string { function entityUrl(context: BadgeContext): string { const e = context.entity!; - const entityUri = `${e.metadata.namespace || ENTITY_DEFAULT_NAMESPACE}/${ - e.kind - }/${e.metadata.name}`; + const entityUri = `${e.metadata.namespace || DEFAULT_NAMESPACE}/${e.kind}/${ + e.metadata.name + }`; const catalogUrl = `${context.config.getString('app.baseUrl')}/catalog`; return `${catalogUrl}/${entityUri}`.toLowerCase(); } diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 286a2335f1..56dbfb0655 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -61,13 +61,13 @@ describe('createRouter', () => { addLocation: jest.fn(), getEntities: jest.fn(), getEntityByName: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; config = new ConfigReader({ diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index 8f2a3b2e84..e624dc36e9 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-badges +## 0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.23 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.2.22 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 5250318164..612b9f3b2d 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.22", + "version": "0.2.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -17,22 +20,22 @@ "directory": "plugins/badges" }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,10 +46,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/badges/src/api/BadgesClient.ts b/plugins/badges/src/api/BadgesClient.ts index 68714b2fa4..f63003dc90 100644 --- a/plugins/badges/src/api/BadgesClient.ts +++ b/plugins/badges/src/api/BadgesClient.ts @@ -16,7 +16,7 @@ import { generatePath } from 'react-router'; import { ResponseError } from '@backstage/errors'; -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { BadgesApi, BadgeSpec } from './types'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; @@ -63,7 +63,7 @@ export class BadgesClient implements BadgesApi { kind: entity.kind.toLocaleLowerCase('en-US'), namespace: entity.metadata.namespace?.toLocaleLowerCase('en-US') ?? - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, name: entity.metadata.name, }; } diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index fdd63ff59b..50dc8b2ad3 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-bazaar-backend +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/backend-test-utils@0.1.19 + +## 0.1.10 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/backend-test-utils@0.1.18 + - @backstage/config@0.1.14 + ## 0.1.9 ### Patch Changes diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 71980acea4..cb596770d4 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.9", + "version": "0.1.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,19 +10,22 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/backend-test-utils": "^0.1.17", - "@backstage/config": "^0.1.13", + "@backstage/backend-common": "^0.11.0", + "@backstage/backend-test-utils": "^0.1.19", + "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -31,7 +34,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2" + "@backstage/cli": "^0.14.1" }, "files": [ "dist", diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index 00f7605888..df72f76d6a 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-bazaar +## 0.1.15 + +### Patch Changes + +- 67a7c02d26: Remove usages of `EntityRef` and `parseEntityName` from `@backstage/catalog-model` +- Updated dependencies + - @backstage/plugin-catalog@0.9.0 + - @backstage/cli@0.14.1 + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.14 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/cli@0.14.0 + - @backstage/catalog-client@0.7.0 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog@0.8.0 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + ## 0.1.13 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index bfdcb18ca3..a38cd1cd16 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.1.13", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,24 +10,27 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.6.0", - "@backstage/catalog-model": "^0.9.10", - "@backstage/cli": "^0.13.2", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog": "^0.7.12", - "@backstage/plugin-catalog-react": "^0.6.14", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/cli": "^0.14.1", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog": "^0.9.0", + "@backstage/plugin-catalog-react": "^0.7.0", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,8 +47,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/dev-utils": "^0.2.21", + "@backstage/cli": "^0.14.1", + "@backstage/dev-utils": "^0.2.23", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.1.5" }, diff --git a/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx b/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx index 5624f40765..ec29d1429b 100644 --- a/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx +++ b/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx @@ -47,7 +47,6 @@ import useAsyncFn from 'react-use/lib/useAsyncFn'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { - parseEntityName, stringifyEntityRef, Entity, parseEntityRef, @@ -150,7 +149,7 @@ export const HomePageBazaarInfoCard = ({ const getEntityPageLink = () => { if (bazaarProject?.value?.entityRef) { - const { name, kind, namespace } = parseEntityName( + const { name, kind, namespace } = parseEntityRef( bazaarProject.value.entityRef, ); return entityLink({ kind, namespace, name }); diff --git a/plugins/bazaar/src/components/SortView/SortView.tsx b/plugins/bazaar/src/components/SortView/SortView.tsx index fa97ac69ee..f5bb6facd7 100644 --- a/plugins/bazaar/src/components/SortView/SortView.tsx +++ b/plugins/bazaar/src/components/SortView/SortView.tsx @@ -107,7 +107,7 @@ export const SortView = () => { if (catalogEntityRefs) { bazaarProjects.value?.forEach(async (project: BazaarProject) => { if (project.entityRef) { - if (!catalogEntityRefs?.includes(project.entityRef as string)) { + if (!catalogEntityRefs?.includes(project.entityRef)) { await bazaarApi.updateProject({ ...project, entityRef: null, diff --git a/plugins/bazaar/src/types.ts b/plugins/bazaar/src/types.ts index 30c07286ac..ec3996d1e2 100644 --- a/plugins/bazaar/src/types.ts +++ b/plugins/bazaar/src/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { EntityRef } from '@backstage/catalog-model'; - export type Member = { itemId: number; userId: string; @@ -30,7 +28,7 @@ export type Size = 'small' | 'medium' | 'large'; export type BazaarProject = { name: string; id: number; - entityRef?: EntityRef; + entityRef?: string; community: string; status: Status; description: string; diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 3fc26f7da7..8af11aa8a4 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-bitrise +## 0.1.27 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.26 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.1.25 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 8b50459a84..228eed3e9b 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.25", + "version": "0.1.27", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,22 +10,25 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -40,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@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-aws/.eslintrc.js b/plugins/catalog-backend-module-aws/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/catalog-backend-module-aws/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md new file mode 100644 index 0000000000..749ab3a230 --- /dev/null +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -0,0 +1,41 @@ +# @backstage/plugin-catalog-backend-module-aws + +## 0.1.0 + +### Minor Changes + +- 25e97e7242: Added this new catalog module, initially containing only the + `AwsOrganizationCloudAccountProcessor`. + + Note that this was moved over from the catalog backend itself, and therefore is + no longer part of its builtin set of processors. If you were using this + processor, through making use of the location type `aws-cloud-accounts` and/or + using the configuration key `catalog.processors.awsOrganization`, you will from + now on have to add the processor manually to your catalog. + + First, add the `@backstage/plugin-catalog-backend-module-aws` dependency to your + `packages/backend` package. + + Then, in `packages/backend/src/plugins/catalog.ts`: + + ```diff + +import { AwsOrganizationCloudAccountProcessor } from '@backstage/plugin-catalog-backend-module-aws'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + + builder.addProcessor( + + AwsOrganizationCloudAccountProcessor.fromConfig( + + env.config, + + { logger: env.logger } + + ) + + ); + // ... + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.22.0 + - @backstage/catalog-model@0.11.0 diff --git a/plugins/catalog-backend-module-aws/README.md b/plugins/catalog-backend-module-aws/README.md new file mode 100644 index 0000000000..304fe7a080 --- /dev/null +++ b/plugins/catalog-backend-module-aws/README.md @@ -0,0 +1,10 @@ +# Catalog Backend Module for LDAP + +This is an extension module to the plugin-catalog-backend plugin, providing an +`AwsOrganizationCloudAccountProcessor` that can be used to ingest cloud accounts +as `Resource` kind entities. + +## Getting started + +See [Backstage documentation](https://backstage.io/docs/integrations/ldap/org) for details on how to install +and configure the plugin. diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md new file mode 100644 index 0000000000..1cab6555fe --- /dev/null +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -0,0 +1,30 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-aws" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; +import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; +import { Config } from '@backstage/config'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { Logger as Logger_2 } from 'winston'; + +// @public +export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger_2; + }, + ): AwsOrganizationCloudAccountProcessor; + // (undocumented) + getProcessorName(): string; + // (undocumented) + readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise; +} +``` diff --git a/plugins/catalog-backend-module-aws/config.d.ts b/plugins/catalog-backend-module-aws/config.d.ts new file mode 100644 index 0000000000..bb416a576e --- /dev/null +++ b/plugins/catalog-backend-module-aws/config.d.ts @@ -0,0 +1,36 @@ +/* + * 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 interface Config { + catalog?: { + /** + * List of processor-specific options and attributes + */ + processors?: { + /** + * AwsOrganizationCloudAccountProcessor configuration + */ + awsOrganization?: { + provider: { + /** + * The role to be assumed by this processor + */ + roleArn?: string; + }; + }; + }; + }; +} diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json new file mode 100644 index 0000000000..ebe77263ac --- /dev/null +++ b/plugins/catalog-backend-module-aws/package.json @@ -0,0 +1,55 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-aws", + "description": "A Backstage catalog backend module that helps integrate towards AWS", + "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" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-aws" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/types": "^0.1.3", + "aws-sdk": "^2.840.0", + "lodash": "^4.17.21", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.14.1", + "@types/lodash": "^4.14.151", + "aws-sdk-mock": "^5.2.1" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.test.ts b/plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.test.ts rename to plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.ts b/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.ts rename to plugins/catalog-backend-module-aws/src/awsOrganization/config.ts diff --git a/plugins/catalog-backend-module-aws/src/index.ts b/plugins/catalog-backend-module-aws/src/index.ts new file mode 100644 index 0000000000..62c10a0f6d --- /dev/null +++ b/plugins/catalog-backend-module-aws/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * A Backstage catalog backend module that helps integrate towards AWS + * + * @packageDocumentation + */ + +export * from './processors'; diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts similarity index 96% rename from plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts rename to plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts index 88b033c33f..a7da6d9ad3 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts @@ -15,13 +15,11 @@ */ import { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor'; -import * as winston from 'winston'; describe('AwsOrganizationCloudAccountProcessor', () => { describe('readLocation', () => { - const processor = new AwsOrganizationCloudAccountProcessor({ + const processor = new (AwsOrganizationCloudAccountProcessor as any)({ provider: {}, - logger: winston.createLogger(), }); const location = { type: 'aws-cloud-accounts', target: '' }; const emit = jest.fn(); diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts similarity index 79% rename from plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts rename to plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts index 655f99d14a..1018e49a41 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts @@ -13,35 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { LocationSpec, ResourceEntityV1alpha1 } from '@backstage/catalog-model'; + +import { ResourceEntityV1alpha1 } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '@backstage/plugin-catalog-backend'; import AWS, { Credentials, Organizations } from 'aws-sdk'; import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations'; - -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; -import { Config } from '@backstage/config'; import { Logger } from 'winston'; import { AwsOrganizationProviderConfig, readAwsOrganizationConfig, -} from './awsOrganization/config'; +} from '../awsOrganization/config'; const AWS_ORGANIZATION_REGION = 'us-east-1'; const LOCATION_TYPE = 'aws-cloud-accounts'; -const ACCOUNTID_ANNOTATION: string = 'amazonaws.com/account-id'; -const ARN_ANNOTATION: string = 'amazonaws.com/arn'; -const ORGANIZATION_ANNOTATION: string = 'amazonaws.com/organization-id'; +const ACCOUNTID_ANNOTATION = 'amazonaws.com/account-id'; +const ARN_ANNOTATION = 'amazonaws.com/arn'; +const ORGANIZATION_ANNOTATION = 'amazonaws.com/organization-id'; /** * A processor for ingesting AWS Accounts from AWS Organizations. * - * If custom authentication is needed, it can be achieved by configuring the global AWS.credentials object. + * If custom authentication is needed, it can be achieved by configuring the + * global AWS.credentials object. + * + * @public */ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { - logger: Logger; - organizations: Organizations; - provider: AwsOrganizationProviderConfig; + private readonly organizations: Organizations; + private readonly provider: AwsOrganizationProviderConfig; static fromConfig(config: Config, options: { logger: Logger }) { const c = config.getOptionalConfig('catalog.processors.awsOrganization'); @@ -67,12 +73,8 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { }); } - constructor(options: { - provider: AwsOrganizationProviderConfig; - logger: Logger; - }) { + private constructor(options: { provider: AwsOrganizationProviderConfig }) { this.provider = options.provider; - this.logger = options.logger; const credentials = AwsOrganizationCloudAccountProcessor.buildCredentials( this.provider, ); @@ -82,64 +84,8 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { }); // Only available in us-east-1 } - normalizeName(name: string): string { - return name - .trim() - .toLocaleLowerCase() - .replace(/[^a-zA-Z0-9\-]/g, '-'); - } - - extractInformationFromArn(arn: string): { - accountId: string; - organizationId: string; - } { - const parts = arn.split('/'); - - return { - accountId: parts[parts.length - 1], - organizationId: parts[parts.length - 2], - }; - } - - async getAwsAccounts(): Promise { - let awsAccounts: Account[] = []; - let isInitialAttempt = true; - let nextToken = undefined; - while (isInitialAttempt || nextToken) { - isInitialAttempt = false; - const orgAccounts: ListAccountsResponse = await this.organizations - .listAccounts({ NextToken: nextToken }) - .promise(); - if (orgAccounts.Accounts) { - awsAccounts = awsAccounts.concat(orgAccounts.Accounts); - } - nextToken = orgAccounts.NextToken; - } - - return awsAccounts; - } - - mapAccountToComponent(account: Account): ResourceEntityV1alpha1 { - const { accountId, organizationId } = this.extractInformationFromArn( - account.Arn as string, - ); - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Resource', - metadata: { - annotations: { - [ACCOUNTID_ANNOTATION]: accountId, - [ARN_ANNOTATION]: account.Arn || '', - [ORGANIZATION_ANNOTATION]: organizationId, - }, - name: this.normalizeName(account.Name || ''), - namespace: 'default', - }, - spec: { - type: 'cloud-account', - owner: 'unknown', - }, - }; + getProcessorName(): string { + return 'AwsOrganizationCloudAccountProcessor'; } async readLocation( @@ -165,10 +111,70 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { } return true; }) - .forEach((entity: ResourceEntityV1alpha1) => { - emit(results.entity(location, entity)); + .forEach(entity => { + emit(processingResult.entity(location, entity)); }); return true; } + + private normalizeName(name: string): string { + return name + .trim() + .toLocaleLowerCase('en-US') + .replace(/[^a-zA-Z0-9\-]/g, '-'); + } + + private extractInformationFromArn(arn: string): { + accountId: string; + organizationId: string; + } { + const parts = arn.split('/'); + + return { + accountId: parts[parts.length - 1], + organizationId: parts[parts.length - 2], + }; + } + + private async getAwsAccounts(): Promise { + let awsAccounts: Account[] = []; + let isInitialAttempt = true; + let nextToken = undefined; + while (isInitialAttempt || nextToken) { + isInitialAttempt = false; + const orgAccounts: ListAccountsResponse = await this.organizations + .listAccounts({ NextToken: nextToken }) + .promise(); + if (orgAccounts.Accounts) { + awsAccounts = awsAccounts.concat(orgAccounts.Accounts); + } + nextToken = orgAccounts.NextToken; + } + + return awsAccounts; + } + + private mapAccountToComponent(account: Account): ResourceEntityV1alpha1 { + const { accountId, organizationId } = this.extractInformationFromArn( + account.Arn as string, + ); + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Resource', + metadata: { + annotations: { + [ACCOUNTID_ANNOTATION]: accountId, + [ARN_ANNOTATION]: account.Arn || '', + [ORGANIZATION_ANNOTATION]: organizationId, + }, + name: this.normalizeName(account.Name || ''), + namespace: 'default', + }, + spec: { + type: 'cloud-account', + owner: 'unknown', + }, + }; + } } diff --git a/plugins/techdocs/src/components/DocsResultListItem/index.ts b/plugins/catalog-backend-module-aws/src/processors/index.ts similarity index 86% rename from plugins/techdocs/src/components/DocsResultListItem/index.ts rename to plugins/catalog-backend-module-aws/src/processors/index.ts index 4e1e511ea1..fa858aa744 100644 --- a/plugins/techdocs/src/components/DocsResultListItem/index.ts +++ b/plugins/catalog-backend-module-aws/src/processors/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { DocsResultListItem } from './DocsResultListItem'; +export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor'; diff --git a/plugins/catalog-backend-module-aws/src/setupTests.ts b/plugins/catalog-backend-module-aws/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/catalog-backend-module-aws/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/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 55162c6003..e05e4936a8 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.14 + +### Patch Changes + +- ed09ad8093: Updated usage of the `LocationSpec` type from `@backstage/catalog-model`, which is deprecated. +- 25e97e7242: Minor wording update +- df61ca71dd: Implemented required `getProcessorName` method for catalog processor. +- Updated dependencies + - @backstage/plugin-catalog-backend@0.22.0 + - @backstage/catalog-model@0.11.0 + +## 0.3.13 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 244d24ebc4: Import `Location` from the `@backstage/catalog-client` package. +- 27eccab216: Replaces use of deprecated catalog-model constants. +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.4 + - @backstage/errors@0.2.1 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.3.12 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index e2dbef74e5..f6b2475e65 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -11,7 +11,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; -import { LocationSpec } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger as Logger_2 } from 'winston'; import { SearchEntry } from 'ldapjs'; import { SearchOptions } from 'ldapjs'; @@ -137,6 +137,8 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { }, ): LdapOrgReaderProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, _optional: boolean, diff --git a/plugins/catalog-backend-module-ldap/config.d.ts b/plugins/catalog-backend-module-ldap/config.d.ts index 57781fd711..df167bc520 100644 --- a/plugins/catalog-backend-module-ldap/config.d.ts +++ b/plugins/catalog-backend-module-ldap/config.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; export interface Config { /** diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index a499049265..dc2f56aad6 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.12", + "description": "A Backstage catalog backend module that helps integrate towards LDAP", + "version": "0.3.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin-module" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,26 +24,27 @@ "backstage" ], "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-backend": "^0.21.3", - "@backstage/types": "^0.1.1", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/types": "^0.1.3", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", "lodash": "^4.17.21", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-ldap/src/index.ts b/plugins/catalog-backend-module-ldap/src/index.ts index a3b3c46815..243044369c 100644 --- a/plugins/catalog-backend-module-ldap/src/index.ts +++ b/plugins/catalog-backend-module-ldap/src/index.ts @@ -15,7 +15,7 @@ */ /** - * A Backstage catalog backend modules that helps integrate towards LDAP + * A Backstage catalog backend module that helps integrate towards LDAP * * @packageDocumentation */ diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index 91fbd87c39..414d043d3d 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -106,6 +106,12 @@ export class LdapClient { reject(new Error(errorString(e))); }); + res.on('page', (_result, cb) => { + if (cb) { + cb(); + } + }); + res.on('end', r => { if (!r) { reject(new Error('Null response')); 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-ldap/src/processors/LdapOrgReaderProcessor.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts index be2d748840..bec24a1744 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { Logger } from 'winston'; import { @@ -28,7 +27,8 @@ import { import { CatalogProcessor, CatalogProcessorEmit, - results, + LocationSpec, + processingResult, } from '@backstage/plugin-catalog-backend'; /** @@ -69,6 +69,10 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { this.userTransformer = options.userTransformer; } + getProcessorName(): string { + return 'LdapOrgReaderProcessor'; + } + async readLocation( location: LocationSpec, _optional: boolean, @@ -115,10 +119,10 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { // Done! for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } for (const user of users) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 847fbae9d4..d2e9facfb5 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.17 + +### Patch Changes + +- ed09ad8093: Updated usage of the `LocationSpec` type from `@backstage/catalog-model`, which is deprecated. +- 25e97e7242: Minor wording update +- a097678475: add configuration to use search criteria to select groups +- df61ca71dd: Implemented required `getProcessorName` method for catalog processor. +- Updated dependencies + - @backstage/plugin-catalog-backend@0.22.0 + - @backstage/catalog-model@0.11.0 + +## 0.2.16 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 27eccab216: Replaces use of deprecated catalog-model constants. +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.4 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + ## 0.2.15 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index 0e088d3a4e..daaf1ec225 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -35,17 +35,32 @@ catalog: # the App registration in the Microsoft Azure Portal. clientId: ${MICROSOFT_GRAPH_CLIENT_ID} clientSecret: ${MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN} + # Optional parameter to include the expanded resource or collection referenced + # by a single relationship (navigation property) in your results. + # Only one relationship can be expanded in a single request. + # See https://docs.microsoft.com/en-us/graph/query-parameters#expand-parameter + # Can be combined with userGroupMember[...] instead of userFilter. + userExpand: manager # Optional filter for user, see Microsoft Graph API for the syntax # See https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties # and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter # This and userGroupMemberFilter are mutually exclusive, only one can be specified userFilter: accountEnabled eq true and userType eq 'member' # Optional filter for users, use group membership to get users. + # (Filtered groups and fetch their members.) # This and userFilter are mutually exclusive, only one can be specified + # See https://docs.microsoft.com/en-us/graph/search-query-parameter userGroupMemberFilter: "displayName eq 'Backstage Users'" + # Optional search for users, use group membership to get users. + # (Search for groups and fetch their members.) + # This and userFilter are mutually exclusive, only one can be specified + userGroupMemberSearch: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' # Optional filter for group, see Microsoft Graph API for the syntax # See https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') + # Optional search for groups, see Microsoft Graph API for the syntax + # See https://docs.microsoft.com/en-us/graph/search-query-parameter + groupSearch: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' ``` `userFilter` and `userGroupMemberFilter` are mutually exclusive, only one can be provided. If both are provided, an error will be thrown. diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 4f5d247a06..574b948dea 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -9,7 +9,7 @@ import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; -import { LocationSpec } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger as Logger_2 } from 'winston'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import * as msal from '@azure/msal-node'; @@ -78,11 +78,21 @@ export class MicrosoftGraphClient { userId: string, maxSize: number, ): Promise; - getUserProfile(userId: string): Promise; + getUserProfile( + userId: string, + query?: ODataQuery, + ): Promise; getUsers(query?: ODataQuery): AsyncIterable; - requestApi(path: string, query?: ODataQuery): Promise; + requestApi( + path: string, + query?: ODataQuery, + headers?: Record, + ): Promise; requestCollection(path: string, query?: ODataQuery): AsyncIterable; - requestRaw(url: string): Promise; + requestRaw( + url: string, + headers?: Record, + ): Promise; } // @public @@ -134,6 +144,8 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { }, ): MicrosoftGraphOrgReaderProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, _optional: boolean, @@ -149,9 +161,11 @@ export type MicrosoftGraphProviderConfig = { clientId: string; clientSecret: string; userFilter?: string; - userExpand?: string[]; + userExpand?: string; userGroupMemberFilter?: string; + userGroupMemberSearch?: string; groupFilter?: string; + groupSearch?: string; }; // @public @@ -159,8 +173,9 @@ export function normalizeEntityName(name: string): string; // @public export type ODataQuery = { + search?: string; filter?: string; - expand?: string[]; + expand?: string; select?: string[]; }; @@ -179,9 +194,11 @@ export function readMicrosoftGraphOrg( client: MicrosoftGraphClient, tenantId: string, options: { - userExpand?: string[]; + userExpand?: string; userFilter?: string; + userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; diff --git a/plugins/catalog-backend-module-msgraph/config.d.ts b/plugins/catalog-backend-module-msgraph/config.d.ts index c7eb5ae8e4..d47abf60bb 100644 --- a/plugins/catalog-backend-module-msgraph/config.d.ts +++ b/plugins/catalog-backend-module-msgraph/config.d.ts @@ -73,12 +73,24 @@ export interface Config { * E.g. "securityEnabled eq false and mailEnabled eq true" */ groupFilter?: string; + /** + * The search criteria to apply to extract users by groups memberships. + * + * E.g. "\"displayName:-team\"" would only match groups which contain '-team' + */ + groupSearch?: string; /** * The filter to apply to extract users by groups memberships. * * E.g. "displayName eq 'Backstage Users'" */ userGroupMemberFilter?: string; + /** + * The search criteria to apply to extract groups. + * + * E.g. "\"displayName:-team\"" would only match groups which contain '-team' + */ + userGroupMemberSearch?: string; }>; }; }; diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 7fd0a2d319..5005a16c5b 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.15", + "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", + "version": "0.2.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin-module" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,18 +24,19 @@ "backstage" ], "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { "@azure/msal-node": "^1.1.0", - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/plugin-catalog-backend": "^0.21.3", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/plugin-catalog-backend": "^0.22.0", "@microsoft/microsoft-graph-types": "^2.6.0", "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", @@ -42,9 +46,9 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/cli": "^0.13.2", - "@backstage/test-utils": "^0.2.4", + "@backstage/backend-common": "^0.11.0", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@types/lodash": "^4.14.151", "msw": "^0.35.0" }, diff --git a/plugins/catalog-backend-module-msgraph/src/index.ts b/plugins/catalog-backend-module-msgraph/src/index.ts index 396ccc3e6a..6a7adedc58 100644 --- a/plugins/catalog-backend-module-msgraph/src/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/index.ts @@ -15,7 +15,7 @@ */ /** - * A Backstage catalog backend modules that helps integrate towards Microsoft Graph + * A Backstage catalog backend module that helps integrate towards Microsoft Graph * * @packageDocumentation */ diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index 6413a97c15..94a126e1a7 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -85,7 +85,7 @@ describe('MicrosoftGraphClient', () => { const response = await client.requestApi('users', { filter: 'test eq true', - expand: ['children'], + expand: 'children', select: ['id', 'children'], }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index 827c9fc14d..cba5d9f640 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -27,6 +27,10 @@ import { MicrosoftGraphProviderConfig } from './config'; * @public */ export type ODataQuery = { + /** + * search resources within a collection matching a free-text search expression. + */ + search?: string; /** * filter a collection of resources */ @@ -34,7 +38,7 @@ export type ODataQuery = { /** * specifies the related resources or media streams to be included in line with retrieved resources */ - expand?: string[]; + expand?: string; /** * request a specific set of properties for each entity or complex type */ @@ -102,7 +106,15 @@ export class MicrosoftGraphClient { path: string, query?: ODataQuery, ): AsyncIterable { - let response = await this.requestApi(path, query); + const headers: Record = query?.search + ? { + // Eventual consistency is required to use $search. + // If a new user/group is not found, it'll eventually be imported on a subsequent read + ConsistencyLevel: 'eventual', + } + : {}; + + let response = await this.requestApi(path, query, headers); for (;;) { if (response.status !== 200) { @@ -121,7 +133,7 @@ export class MicrosoftGraphClient { return; } - response = await this.requestRaw(result['@odata.nextLink']); + response = await this.requestRaw(result['@odata.nextLink'], headers); } } @@ -131,13 +143,19 @@ export class MicrosoftGraphClient { * @public * @param path - Resource in Microsoft Graph * @param query - OData Query {@link ODataQuery} + * @param headers - optional HTTP headers */ - async requestApi(path: string, query?: ODataQuery): Promise { + async requestApi( + path: string, + query?: ODataQuery, + headers?: Record, + ): Promise { const queryString = qs.stringify( { + $search: query?.search, $filter: query?.filter, $select: query?.select?.join(','), - $expand: query?.expand?.join(','), + $expand: query?.expand, }, { addQueryPrefix: true, @@ -146,15 +164,22 @@ export class MicrosoftGraphClient { }, ); - return await this.requestRaw(`${this.baseUrl}/${path}${queryString}`); + return await this.requestRaw( + `${this.baseUrl}/${path}${queryString}`, + headers, + ); } /** * Makes a HTTP call to Graph API with token * * @param url - HTTP Endpoint of Graph API + * @param headers - optional HTTP headers */ - async requestRaw(url: string): Promise { + async requestRaw( + url: string, + headers?: Record, + ): Promise { // Make sure that we always have a valid access token (might be cached) const token = await this.pca.acquireTokenByClientCredential({ scopes: ['https://graph.microsoft.com/.default'], @@ -166,6 +191,7 @@ export class MicrosoftGraphClient { return await fetch(url, { headers: { + ...headers, Authorization: `Bearer ${token.accessToken}`, }, }); @@ -177,10 +203,14 @@ export class MicrosoftGraphClient { * * @public * @param userId - The unique identifier for the `User` resource + * @param query - OData Query {@link ODataQuery} * */ - async getUserProfile(userId: string): Promise { - const response = await this.requestApi(`users/${userId}`); + async getUserProfile( + userId: string, + query?: ODataQuery, + ): Promise { + const response = await this.requestApi(`users/${userId}`, query); if (response.status !== 200) { await this.handleError('user profile', response); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index efbbca1f5c..700e5cd8fe 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -53,6 +53,7 @@ describe('readMicrosoftGraphConfig', () => { clientId: 'clientId', clientSecret: 'clientSecret', authority: 'https://login.example.com/', + userExpand: 'manager', userFilter: 'accountEnabled eq true', groupFilter: 'securityEnabled eq false', }, @@ -66,6 +67,7 @@ describe('readMicrosoftGraphConfig', () => { clientId: 'clientId', clientSecret: 'clientSecret', authority: 'https://login.example.com', + userExpand: 'manager', userFilter: 'accountEnabled eq true', groupFilter: 'securityEnabled eq false', }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index c401bc2553..c2789d767a 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -57,19 +57,31 @@ export type MicrosoftGraphProviderConfig = { * * E.g. "manager" */ - userExpand?: string[]; + userExpand?: string; /** * The filter to apply to extract users by groups memberships. * * E.g. "displayName eq 'Backstage Users'" */ userGroupMemberFilter?: string; + /** + * The search criteria to apply to extract users by groups memberships. + * + * E.g. "\"displayName:-team\"" would only match groups which contain '-team' + */ + userGroupMemberSearch?: string; /** * The filter to apply to extract groups. * * E.g. "securityEnabled eq false and mailEnabled eq true" */ groupFilter?: string; + /** + * The search criteria to apply to extract groups. + * + * E.g. "\"displayName:-team\"" would only match groups which contain '-team' + */ + groupSearch?: string; }; /** @@ -94,17 +106,28 @@ export function readMicrosoftGraphConfig( const tenantId = providerConfig.getString('tenantId'); const clientId = providerConfig.getString('clientId'); const clientSecret = providerConfig.getString('clientSecret'); + + const userExpand = providerConfig.getOptionalString('userExpand'); const userFilter = providerConfig.getOptionalString('userFilter'); const userGroupMemberFilter = providerConfig.getOptionalString( 'userGroupMemberFilter', ); + const userGroupMemberSearch = providerConfig.getOptionalString( + 'userGroupMemberSearch', + ); const groupFilter = providerConfig.getOptionalString('groupFilter'); + const groupSearch = providerConfig.getOptionalString('groupSearch'); if (userFilter && userGroupMemberFilter) { throw new Error( `userFilter and userGroupMemberFilter are mutually exclusive, only one can be specified.`, ); } + if (userFilter && userGroupMemberSearch) { + throw new Error( + `userGroupMemberSearch cannot be specified when userFilter is defined.`, + ); + } providers.push({ target, @@ -112,9 +135,12 @@ export function readMicrosoftGraphConfig( tenantId, clientId, clientSecret, + userExpand, userFilter, userGroupMemberFilter, + userGroupMemberSearch, groupFilter, + groupSearch, }); } diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index b0273e1f58..dba3741c73 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -118,7 +118,7 @@ describe('read microsoft graph', () => { expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); - it('should read users with custom transformer', async () => { + it('should read users with userExpand and custom transformer', async () => { async function* getExampleUsers() { yield { id: 'userid', @@ -133,6 +133,7 @@ describe('read microsoft graph', () => { ); const { users } = await readMicrosoftGraphUsers(client, { + userExpand: 'manager', userFilter: 'accountEnabled eq true', transformer: async () => ({ apiVersion: 'backstage.io/v1alpha1', @@ -154,6 +155,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toBeCalledTimes(1); expect(client.getUsers).toBeCalledWith({ + expand: 'manager', filter: 'accountEnabled eq true', }); expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); @@ -227,12 +229,14 @@ describe('read microsoft graph', () => { expect(client.getGroupMembers).toBeCalledWith('groupid'); expect(client.getUserProfile).toBeCalledTimes(1); - expect(client.getUserProfile).toBeCalledWith('userid'); + expect(client.getUserProfile).toBeCalledWith('userid', { + expand: undefined, + }); expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); - it('should read users with custom transformer', async () => { + it('should read users with userExpand and custom transformer', async () => { async function* getExampleGroups() { yield { id: 'groupid', @@ -266,6 +270,7 @@ describe('read microsoft graph', () => { ); const { users } = await readMicrosoftGraphUsersInGroups(client, { + userExpand: 'manager', userGroupMemberFilter: 'securityEnabled eq true', transformer: async () => ({ apiVersion: 'backstage.io/v1alpha1', @@ -293,7 +298,9 @@ describe('read microsoft graph', () => { expect(client.getGroupMembers).toBeCalledWith('groupid'); expect(client.getUserProfile).toBeCalledTimes(1); - expect(client.getUserProfile).toBeCalledWith('userid'); + expect(client.getUserProfile).toBeCalledWith('userid', { + expand: 'manager', + }); expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); @@ -634,6 +641,14 @@ describe('read microsoft graph', () => { }; } + async function getExampleUserProfile(userId: string) { + return { + id: userId, + displayName: 'User Name', + mail: 'user.name@example.com', + }; + } + async function* getExampleGroups() { yield { id: 'groupid', @@ -686,7 +701,7 @@ describe('read microsoft graph', () => { }); }); - it('should read users using userFilter', async () => { + it('should read users using userExpand and userFilter', async () => { client.getOrganization.mockResolvedValue({ id: 'tenantid', displayName: 'Organization Name', @@ -705,12 +720,14 @@ describe('read microsoft graph', () => { await readMicrosoftGraphOrg(client, 'tenantid', { logger: getVoidLogger(), + userExpand: 'manager', userFilter: 'accountEnabled eq true', groupFilter: 'securityEnabled eq false', }); expect(client.getUsers).toBeCalledTimes(1); expect(client.getUsers).toBeCalledWith({ + expand: 'manager', filter: 'accountEnabled eq true', }); expect(client.getGroups).toBeCalledTimes(1); @@ -719,13 +736,14 @@ describe('read microsoft graph', () => { }); }); - it('should read users using userGroupMemberFilter', async () => { + it('should read users using userExpand and userGroupMemberFilter', async () => { client.getOrganization.mockResolvedValue({ id: 'tenantid', displayName: 'Organization Name', }); client.getUsers.mockImplementation(getExampleUsers); + client.getUserProfile.mockImplementation(getExampleUserProfile); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -750,6 +768,8 @@ describe('read microsoft graph', () => { expect(client.getGroups).toBeCalledWith({ filter: 'securityEnabled eq false', }); + expect(client.getUserProfile).toBeCalledTimes(1); + expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); }); }); }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 58e60ab76a..333eaa6b0a 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -85,7 +85,7 @@ export async function readMicrosoftGraphUsers( client: MicrosoftGraphClient, options: { userFilter?: string; - userExpand?: string[]; + userExpand?: string; transformer?: UserTransformer; logger: Logger; }, @@ -137,6 +137,8 @@ export async function readMicrosoftGraphUsers( export async function readMicrosoftGraphUsersInGroups( client: MicrosoftGraphClient, options: { + userExpand?: string; + userGroupMemberSearch?: string; userGroupMemberFilter?: string; transformer?: UserTransformer; logger: Logger; @@ -155,6 +157,7 @@ export async function readMicrosoftGraphUsersInGroups( const groupMemberUsers: Set = new Set(); for await (const group of client.getGroups({ + search: options?.userGroupMemberSearch, filter: options?.userGroupMemberFilter, })) { // Process all groups in parallel, otherwise it can take quite some time @@ -184,7 +187,9 @@ export async function readMicrosoftGraphUsersInGroups( let user; let userPhoto; try { - user = await client.getUserProfile(userId); + user = await client.getUserProfile(userId, { + expand: options.userExpand, + }); } catch (e) { options.logger.warn(`Unable to load user for ${userId}`); } @@ -324,6 +329,7 @@ export async function readMicrosoftGraphGroups( client: MicrosoftGraphClient, tenantId: string, options?: { + groupSearch?: string; groupFilter?: string; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -351,6 +357,7 @@ export async function readMicrosoftGraphGroups( const promises: Promise[] = []; for await (const group of client.getGroups({ + search: options?.groupSearch, filter: options?.groupFilter, })) { // Process all groups in parallel, otherwise it can take quite some time @@ -502,9 +509,11 @@ export async function readMicrosoftGraphOrg( client: MicrosoftGraphClient, tenantId: string, options: { - userExpand?: string[]; + userExpand?: string; userFilter?: string; + userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; @@ -519,6 +528,7 @@ export async function readMicrosoftGraphOrg( client, { userGroupMemberFilter: options.userGroupMemberFilter, + userGroupMemberSearch: options.userGroupMemberSearch, transformer: options.userTransformer, logger: options.logger, }, @@ -535,6 +545,7 @@ export async function readMicrosoftGraphOrg( } const { groups, rootGroup, groupMember, groupMemberOf } = await readMicrosoftGraphGroups(client, tenantId, { + groupSearch: options?.groupSearch, groupFilter: options?.groupFilter, groupTransformer: options?.groupTransformer, organizationTransformer: options?.organizationTransformer, 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..7d27e87d73 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 { @@ -123,7 +123,9 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { { userFilter: provider.userFilter, userGroupMemberFilter: provider.userGroupMemberFilter, + userGroupMemberSearch: provider.userGroupMemberSearch, groupFilter: provider.groupFilter, + groupSearch: provider.groupSearch, groupTransformer: this.options.groupTransformer, userTransformer: this.options.userTransformer, organizationTransformer: this.options.organizationTransformer, @@ -180,8 +182,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 e0e63d86fc..942bde19eb 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { CatalogProcessor, CatalogProcessorEmit, - results, + LocationSpec, + processingResult, } from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; import { @@ -73,6 +73,9 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { this.groupTransformer = options.groupTransformer; this.organizationTransformer = options.organizationTransformer; } + getProcessorName(): string { + return 'MicrosoftGraphOrgReaderProcessor'; + } async readLocation( location: LocationSpec, @@ -105,7 +108,9 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { userExpand: provider.userExpand, userFilter: provider.userFilter, userGroupMemberFilter: provider.userGroupMemberFilter, + userGroupMemberSearch: provider.userGroupMemberSearch, groupFilter: provider.groupFilter, + groupSearch: provider.groupSearch, userTransformer: this.userTransformer, groupTransformer: this.groupTransformer, organizationTransformer: this.organizationTransformer, @@ -120,10 +125,10 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { // Done! for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } for (const user of users) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index a371712c80..d80acc6d23 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,174 @@ # @backstage/plugin-catalog-backend +## 0.22.0 + +### Minor Changes + +- 209fd128e6: The `CodeOwnersProcessor` no longer supports the deprecated SCM-specific location types like `'github/api'`. This is a breaking change but it is unlikely to have an impact, as these location types haven't been supported by the rest of the catalog for a long time. +- 9876e7f172: **BREAKING**: Removed unused `durationText` utility. +- 25e97e7242: **BREAKING**: Removed `AwsOrganizationCloudAccountProcessor` from the default + set of builtin processors, and instead moved it into its own module + `@backstage/plugin-catalog-backend-module-aws`. + + If you were using this processor, through making use of the location type + `aws-cloud-accounts` and/or using the configuration key + `catalog.processors.awsOrganization`, you will from now on have to add the + processor manually to your catalog. + + First, add the `@backstage/plugin-catalog-backend-module-aws` dependency to your + `packages/backend` package. + + Then, in `packages/backend/src/plugins/catalog.ts`: + + ```diff + +import { AwsOrganizationCloudAccountProcessor } from '@backstage/plugin-catalog-backend-module-aws'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + + builder.addProcessor( + + AwsOrganizationCloudAccountProcessor.fromConfig( + + env.config, + + { logger: env.logger } + + ) + + ); + // ... + ``` + +- e9cf0dd03e: Made the `GitLabDiscoveryProcessor.updateLastActivity` method private, as it was accidentally exposed. It has also been fixed to properly operate in its own cache namespace to avoid collisions with other processors. +- df61ca71dd: Updated all processors to implement `getProcessorName`. + + **BREAKING**: The `CatalogProcessor` interface now require that the `CatalogProcessor` class implements `getProcessorName()`. + The processor name has previously defaulted processor class name. It's therefore _recommended_ to keep your return the same name as the class name if you did not implement this method previously. + + For example: + + ```ts + class CustomProcessor implements CatalogProcessor { + getProcessorName() { + // Use the same name as the class name if this method was not previously implemented. + return 'CustomProcessor'; + } + } + ``` + +### Patch Changes + +- 919cf2f836: The catalog API now returns entity relations that have three fields: The old + `type` and `target` fields, as well as a new `targetRef` field. The last one is + the stringified form of the second one. + + **DEPRECATION**: The `target` field is hereby deprecated, both as seen from the + catalog API as well as from the `@backstage/catalog-model` package. Both + `target` and `targetRef` will be produced for some time, but eventually, + `target` will be removed entirely. Please update your readers to stop consuming + the `relations.[].target` field from the catalog API as soon as possible. + +- 957cb4cb20: Deprecated the `runPeriodically` function which is no longer in use. +- 01e124ea60: Added an `/entity-facets` endpoint, which lets you query the distribution of + possible values for fields of entities. + + This can be useful for example when populating a dropdown in the user interface, + such as listing all tag values that are actually being used right now in your + catalog instance, along with putting the most common ones at the top. + +- 082c32f948: Deprecated the second parameter of `results.location()` that determines whether an emitted location is optional. In cases where this is currently being set to `false`, the parameter can simply be dropped, as that is the default. Usage where this was being set to `true` should be migrated to set the `presence` option of the emitted location to `optional`. For example: + + ```ts + results.location( + { + type: 'url', + target: 'http://example.com/foo', + }, + true, + ); + + // migrated to + + results.location({ + type: 'url', + target: 'http://example.com/foo', + presence: 'optional', + }); + ``` + +- ed09ad8093: Added `LocationSpec`, which was moved over from `@backstage/catalog-model`. + + Added `LocationInput`, which replaced `LocationSpec` where it was used in the `LocationService` and `LocationStore` interfaces. The `LocationInput` type deprecates the `presence` field, which was not being used in those contexts. + +- 6d994fd9da: Cleanup catalog-backend API report. +- 7010349c9a: Added `EntityRelationSpec`, which was moved over from `@backstage/catalog-model`. +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- 420f8d710f: Removed the `processors.githubOrg` config section which is unused and has been replaced by the integrations config. +- b1296f1f57: Deprecated `StaticLocationProcessor` which is unused and replaced by `ConfigLocationEntityProvider`. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-scaffolder-common@0.2.2 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/plugin-permission-node@0.5.2 + - @backstage/integration@0.7.5 + +## 0.21.5 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-client@0.7.1 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + - @backstage/search-common@0.2.4 + - @backstage/types@0.1.3 + - @backstage/plugin-catalog-common@0.1.4 + - @backstage/plugin-permission-common@0.5.1 + - @backstage/plugin-permission-node@0.5.1 + - @backstage/plugin-scaffolder-common@0.2.1 + +## 0.21.4 + +### Patch Changes + +- 379da9fb1d: 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` + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 538ca90790: Use updated type names from `@backstage/catalog-client` +- ca1d6c1788: Support "dependencyOf" relation in Resource entities +- 244d24ebc4: Import `Location` from the `@backstage/catalog-client` package. +- e483dd6c72: Update internal `Location` validation. +- 216725b434: Updated to use new names for `parseLocationRef` and `stringifyLocationRef` +- e72d371296: Use `TemplateEntityV1beta2` from `@backstage/plugin-scaffolder-common` instead + of `@backstage/catalog-model`. +- 27eccab216: Replaces use of deprecated catalog-model constants. +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- b590e9b58d: Optimized entity provider mutations with large numbers of new additions, such as big initial startup commits +- Updated dependencies + - @backstage/plugin-scaffolder-common@0.2.0 + - @backstage/backend-common@0.10.8 + - @backstage/catalog-client@0.7.0 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/plugin-permission-common@0.5.0 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/search-common@0.2.3 + - @backstage/types@0.1.2 + - @backstage/plugin-catalog-common@0.1.3 + - @backstage/plugin-permission-node@0.5.0 + ## 0.21.3 ### Patch Changes @@ -725,16 +894,16 @@ ### Minor Changes -- 8bfc0571c: Add a default catalog value for BitBucketDiscoveryProcessor. This allows to have a target like so: https://bitbucket.mycompany.com/projects/backstage/repos/service-* - which will be expanded to https://bitbucket.mycompany.com/projects/backstage/repos/service-a/catalog-info.yaml given that repository 'service-a' exists. +- 8bfc0571c: Add a default catalog value for BitBucketDiscoveryProcessor. This allows to have a target like so: `https://bitbucket.mycompany.com/projects/backstage/repos/service-*` + which will be expanded to `https://bitbucket.mycompany.com/projects/backstage/repos/service-a/catalog-info.yaml` given that repository 'service-a' exists. ## Migration If you are using a custom [Bitbucket parser](https://backstage.io/docs/integrations/bitbucket/discovery#custom-repository-processing) and your `bitbucket-discovery` target (e.g. in your app-config.yaml) omits the catalog path in any of the following ways: - - https://bitbucket.mycompany.com/projects/backstage/repos/service-* - - https://bitbucket.mycompany.com/projects/backstage/repos/* - - https://bitbucket.mycompany.com/projects/backstage/repos/*/ + - `https://bitbucket.mycompany.com/projects/backstage/repos/service-*` + - `https://bitbucket.mycompany.com/projects/backstage/repos/*` + - `https://bitbucket.mycompany.com/projects/backstage/repos/*/` then you will be affected by this change. The 'target' input to your parser before this commit would be '/', and after this commit it will be '/catalog-info.yaml', and as such needs to be handled to maintain the same functionality. @@ -1052,7 +1221,7 @@ - 0fd4ea443: Updates the `GithubCredentialsProvider` to return the token type, it can either be `token` or `app` depending on the authentication method. - Update the `GithubOrgReaderProcessor` NOT to query for email addresses if GitHub Apps is used for authentication, this is due to inconsistencies in the GitHub API when using server to server communications and installation tokens. https://github.community/t/api-v4-unable-to-retrieve-email-resource-not-accessible-by-integration/13831/4 for more info. + Update the `GithubOrgReaderProcessor` NOT to query for email addresses if GitHub Apps is used for authentication, this is due to inconsistencies in the GitHub API when using server to server communications and installation tokens. See [this community discussion](https://github.community/t/api-v4-unable-to-retrieve-email-resource-not-accessible-by-integration/13831/4) for more info. **Removes** deprecated GithubOrgReaderProcessor provider configuration(`catalog.processors.githubOrg`). If you're using the deprecated config section make sure to migrate to [integrations](https://backstage.io/docs/integrations/github/locations) instead. diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index deed3e2d8b..d5541678e6 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -5,27 +5,24 @@ ```ts /// -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'; -import { DocumentCollator } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/search-common'; import { Entity } from '@backstage/catalog-model'; +import { EntityName } 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 { LocationSpec } from '@backstage/catalog-model'; +import { Location as Location_2 } from '@backstage/catalog-client'; import { Logger as Logger_2 } from 'winston'; -import { Organizations } from 'aws-sdk'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionCondition } from '@backstage/plugin-permission-common'; @@ -33,16 +30,13 @@ import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { ResourceEntityV1alpha1 } from '@backstage/catalog-model'; +import { Readable } from 'stream'; 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'; -// Warning: (ae-missing-release-tag) "AnalyzeLocationEntityField" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AnalyzeLocationEntityField = { field: string; @@ -54,44 +48,35 @@ export type AnalyzeLocationEntityField = { description: string; }; -// Warning: (ae-missing-release-tag) "AnalyzeLocationExistingEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AnalyzeLocationExistingEntity = { location: LocationSpec; isRegistered: boolean; entity: Entity; }; -// Warning: (ae-missing-release-tag) "AnalyzeLocationGenerateEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AnalyzeLocationGenerateEntity = { entity: RecursivePartial; fields: AnalyzeLocationEntityField[]; }; -// Warning: (ae-missing-release-tag) "AnalyzeLocationRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AnalyzeLocationRequest = { location: LocationSpec; }; -// Warning: (ae-missing-release-tag) "AnalyzeLocationResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AnalyzeLocationResponse = { existingEntityFiles: AnalyzeLocationExistingEntity[]; generateEntities: AnalyzeLocationGenerateEntity[]; }; -// Warning: (ae-missing-release-tag) "AnnotateLocationEntityProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class AnnotateLocationEntityProcessor implements CatalogProcessor { - // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts - constructor(options: Options); + constructor(options: { integrations: ScmIntegrationRegistry }); + // (undocumented) + getProcessorName(): string; // (undocumented) preProcessEntity( entity: Entity, @@ -101,70 +86,23 @@ export class AnnotateLocationEntityProcessor implements CatalogProcessor { ): Promise; } -// Warning: (ae-missing-release-tag) "AnnotateScmSlugEntityProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { constructor(opts: { scmIntegrationRegistry: ScmIntegrationRegistry }); // (undocumented) static fromConfig(config: Config): AnnotateScmSlugEntityProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) preProcessEntity(entity: Entity, location: LocationSpec): Promise; } -// Warning: (ae-missing-release-tag) "AwsOrganizationCloudAccountProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { - constructor(options: { - provider: AwsOrganizationProviderConfig; - logger: Logger_2; - }); - // (undocumented) - extractInformationFromArn(arn: string): { - accountId: string; - organizationId: string; - }; - // (undocumented) - static fromConfig( - config: Config, - options: { - logger: Logger_2; - }, - ): AwsOrganizationCloudAccountProcessor; - // (undocumented) - getAwsAccounts(): Promise; - // (undocumented) - logger: Logger_2; - // (undocumented) - mapAccountToComponent(account: Account): ResourceEntityV1alpha1; - // (undocumented) - normalizeName(name: string): string; - // (undocumented) - organizations: Organizations; - // (undocumented) - provider: AwsOrganizationProviderConfig; - // (undocumented) - readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise; -} - -// Warning: (ae-missing-release-tag) "AwsOrganizationProviderConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export type AwsOrganizationProviderConfig = { - roleArn?: string; -}; - -// Warning: (ae-missing-release-tag) "AwsS3DiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class AwsS3DiscoveryProcessor implements CatalogProcessor { constructor(reader: UrlReader); // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, optional: boolean, @@ -173,11 +111,12 @@ export class AwsS3DiscoveryProcessor implements CatalogProcessor { ): Promise; } -// Warning: (ae-missing-release-tag) "AzureDevOpsDiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @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, @@ -186,6 +125,8 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { }, ): AzureDevOpsDiscoveryProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, _optional: boolean, @@ -193,24 +134,32 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { ): Promise; } -// Warning: (ae-missing-release-tag) "BitbucketDiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class BitbucketDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - parser?: BitbucketRepositoryParser; + parser?: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger_2; + }) => AsyncIterable; logger: Logger_2; }); // (undocumented) static fromConfig( config: Config, options: { - parser?: BitbucketRepositoryParser; + parser?: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger_2; + }) => AsyncIterable; logger: Logger_2; }, ): BitbucketDiscoveryProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, _optional: boolean, @@ -218,19 +167,17 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { ): Promise; } -// Warning: (ae-missing-release-tag) "BitbucketRepositoryParser" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type BitbucketRepositoryParser = (options: { integration: BitbucketIntegration; target: string; logger: Logger_2; }) => AsyncIterable; -// Warning: (ae-missing-release-tag) "BuiltinKindsEntityProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class BuiltinKindsEntityProcessor implements CatalogProcessor { + // (undocumented) + getProcessorName(): string; // (undocumented) postProcessEntity( entity: Entity, @@ -275,7 +222,7 @@ export class CatalogBuilder { setRefreshIntervalSeconds(seconds: number): CatalogBuilder; } -// @public +// @alpha export const catalogConditions: Conditions<{ hasAnnotation: PermissionRule< Entity, @@ -301,8 +248,6 @@ export const catalogConditions: Conditions<{ >; }>; -// Warning: (ae-missing-release-tag) "CatalogEntityDocument" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface CatalogEntityDocument extends IndexableDocument { // (undocumented) @@ -317,8 +262,6 @@ export interface CatalogEntityDocument extends IndexableDocument { owner: string; } -// Warning: (ae-missing-release-tag) "CatalogEnvironment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogEnvironment = { logger: Logger_2; @@ -328,8 +271,6 @@ export type CatalogEnvironment = { permissions: PermissionAuthorizer; }; -// Warning: (ae-missing-release-tag) "CatalogProcessingEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface CatalogProcessingEngine { // (undocumented) @@ -338,19 +279,15 @@ export interface CatalogProcessingEngine { stop(): Promise; } -// Warning: (ae-missing-release-tag) "CatalogProcessingOrchestrator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface CatalogProcessingOrchestrator { // (undocumented) process(request: EntityProcessingRequest): Promise; } -// Warning: (ae-missing-release-tag) "CatalogProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogProcessor = { - getProcessorName?(): string; + getProcessorName(): string; readLocation?( location: LocationSpec, optional: boolean, @@ -372,11 +309,6 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; - handleError?( - error: Error, - location: LocationSpec, - emit: CatalogProcessorEmit, - ): Promise; }; // @public @@ -385,13 +317,9 @@ export interface CatalogProcessorCache { set(key: string, value: ItemType): Promise; } -// Warning: (ae-missing-release-tag) "CatalogProcessorEmit" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogProcessorEmit = (generated: CatalogProcessorResult) => void; -// Warning: (ae-missing-release-tag) "CatalogProcessorEntityResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogProcessorEntityResult = { type: 'entity'; @@ -399,8 +327,6 @@ export type CatalogProcessorEntityResult = { location: LocationSpec; }; -// Warning: (ae-missing-release-tag) "CatalogProcessorErrorResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogProcessorErrorResult = { type: 'error'; @@ -408,34 +334,25 @@ export type CatalogProcessorErrorResult = { location: LocationSpec; }; -// Warning: (ae-missing-release-tag) "CatalogProcessorLocationResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogProcessorLocationResult = { type: 'location'; location: LocationSpec; - optional: boolean; + optional?: boolean; }; -// Warning: (ae-missing-release-tag) "CatalogProcessorParser" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type CatalogProcessorParser = (options: { data: Buffer; location: LocationSpec; }) => AsyncIterable; -// Warning: (ae-missing-release-tag) "CatalogProcessorRelationResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogProcessorRelationResult = { type: 'relation'; relation: EntityRelationSpec; - entityRef?: string; }; -// Warning: (ae-missing-release-tag) "CatalogProcessorResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogProcessorResult = | CatalogProcessorLocationResult @@ -459,12 +376,10 @@ export type CatalogRulesEnforcer = { isAllowed(entity: Entity, location: LocationSpec): boolean; }; -// Warning: (ae-missing-release-tag) "CodeOwnersProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class CodeOwnersProcessor implements CatalogProcessor { constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger_2; reader: UrlReader; }); @@ -477,21 +392,21 @@ export class CodeOwnersProcessor implements CatalogProcessor { }, ): CodeOwnersProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) preProcessEntity(entity: Entity, location: LocationSpec): Promise; } -// @public +// @alpha export const createCatalogPermissionRule: ( rule: PermissionRule, ) => PermissionRule; -// @public +// @alpha export const createCatalogPolicyDecision: ( conditions: PermissionCriteria>, ) => ConditionalPolicyDecision; -// Warning: (ae-missing-release-tag) "createRandomRefreshInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function createRandomRefreshInterval(options: { minSeconds: number; @@ -501,15 +416,13 @@ export function createRandomRefreshInterval(options: { // @public export function createRouter(options: RouterOptions): Promise; -// Warning: (ae-missing-release-tag) "DefaultCatalogCollator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class DefaultCatalogCollator implements DocumentCollator { +// @public @deprecated (undocumented) +export class DefaultCatalogCollator { constructor(options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; locationTemplate?: string; - filter?: CatalogEntitiesRequest['filter']; + filter?: GetEntitiesRequest['filter']; catalogClient?: CatalogApi; }); // (undocumented) @@ -524,14 +437,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) @@ -544,8 +457,31 @@ export class DefaultCatalogCollator implements DocumentCollator { readonly visibilityPermission: Permission; } -// Warning: (ae-missing-release-tag) "DefaultCatalogProcessingOrchestrator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + static fromConfig( + _config: Config, + options: DefaultCatalogCollatorFactoryOptions, + ): DefaultCatalogCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission; +} + +// @public (undocumented) +export type DefaultCatalogCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + // @public (undocumented) export class DefaultCatalogProcessingOrchestrator implements CatalogProcessingOrchestrator @@ -570,19 +506,12 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { isAllowed(entity: Entity, location: LocationSpec): boolean; } -// Warning: (ae-missing-release-tag) "DeferredEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type DeferredEntity = { entity: Entity; locationKey?: string; }; -// Warning: (ae-missing-release-tag) "durationText" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export function durationText(startTimestamp: [number, number]): string; - // @public (undocumented) export type EntitiesCatalog = { entities(request?: EntitiesRequest): Promise; @@ -598,10 +527,9 @@ export type EntitiesCatalog = { authorizationToken?: string; }, ): Promise; + facets(request: EntityFacetsRequest): Promise; }; -// Warning: (ae-missing-release-tag) "EntitiesRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type EntitiesRequest = { filter?: EntityFilter; @@ -610,25 +538,19 @@ export type EntitiesRequest = { authorizationToken?: string; }; -// Warning: (ae-missing-release-tag) "EntitiesResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type EntitiesResponse = { entities: Entity[]; pageInfo: PageInfo; }; -// Warning: (ae-missing-release-tag) "EntitiesSearchFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntitiesSearchFilter = { key: string; values?: string[]; }; -// Warning: (ae-missing-release-tag) "entity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) function entity( atLocation: LocationSpec, newEntity: Entity, @@ -643,8 +565,24 @@ export type EntityAncestryResponse = { }>; }; -// Warning: (ae-missing-release-tag) "EntityFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export interface EntityFacetsRequest { + authorizationToken?: string; + facets: string[]; + filter?: EntityFilter; +} + +// @public +export interface EntityFacetsResponse { + facets: Record< + string, + Array<{ + value: string; + count: number; + }> + >; +} + // @public export type EntityFilter = | { @@ -658,8 +596,6 @@ export type EntityFilter = } | EntitiesSearchFilter; -// Warning: (ae-missing-release-tag) "EntityPagination" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityPagination = { limit?: number; @@ -667,17 +603,13 @@ export type EntityPagination = { after?: string; }; -// Warning: (ae-missing-release-tag) "EntityProcessingRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type EntityProcessingRequest = { entity: Entity; state?: JsonObject; }; -// Warning: (ae-missing-release-tag) "EntityProcessingResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type EntityProcessingResult = | { ok: true; @@ -692,27 +624,18 @@ export type EntityProcessingResult = errors: Error[]; }; -// Warning: (ae-missing-release-tag) "EntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface EntityProvider { - // (undocumented) connect(connection: EntityProviderConnection): Promise; - // (undocumented) getProviderName(): string; } -// Warning: (ae-missing-release-tag) "EntityProviderConnection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface EntityProviderConnection { - // (undocumented) applyMutation(mutation: EntityProviderMutation): Promise; } -// Warning: (ae-missing-release-tag) "EntityProviderMutation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type EntityProviderMutation = | { type: 'full'; @@ -724,10 +647,17 @@ export type EntityProviderMutation = removed: DeferredEntity[]; }; -// Warning: (ae-missing-release-tag) "FileReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type EntityRelationSpec = { + source: EntityName; + type: string; + target: EntityName; +}; + // @public (undocumented) export class FileReaderProcessor implements CatalogProcessor { + // (undocumented) + getProcessorName(): string; // (undocumented) readLocation( location: LocationSpec, @@ -737,20 +667,16 @@ export class FileReaderProcessor implements CatalogProcessor { ): Promise; } -// Warning: (ae-missing-release-tag) "generalError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) function generalError( atLocation: LocationSpec, message: string, ): CatalogProcessorResult; -// Warning: (ae-missing-release-tag) "GithubDiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class GithubDiscoveryProcessor implements CatalogProcessor { constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger_2; githubCredentialsProvider?: GithubCredentialsProvider; }); @@ -763,6 +689,8 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { }, ): GithubDiscoveryProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, _optional: boolean, @@ -770,10 +698,17 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { ): Promise; } +// @public +export type GithubMultiOrgConfig = Array<{ + name: string; + groupNamespace: string; + userNamespace: string | undefined; +}>; + // @alpha export class GithubMultiOrgReaderProcessor implements CatalogProcessor { constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger_2; orgs: GithubMultiOrgConfig; githubCredentialsProvider?: GithubCredentialsProvider; @@ -787,6 +722,8 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { }, ): GithubMultiOrgReaderProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, _optional: boolean, @@ -794,8 +731,6 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { ): Promise; } -// Warning: (ae-missing-release-tag) "GitHubOrgEntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class GitHubOrgEntityProvider implements EntityProvider { constructor(options: { @@ -823,12 +758,10 @@ export class GitHubOrgEntityProvider implements EntityProvider { read(): Promise; } -// Warning: (ae-missing-release-tag) "GithubOrgReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class GithubOrgReaderProcessor implements CatalogProcessor { constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger_2; githubCredentialsProvider?: GithubCredentialsProvider; }); @@ -841,6 +774,8 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { }, ): GithubOrgReaderProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, _optional: boolean, @@ -848,8 +783,6 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { ): Promise; } -// Warning: (ae-missing-release-tag) "GitLabDiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class GitLabDiscoveryProcessor implements CatalogProcessor { // (undocumented) @@ -860,33 +793,27 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { }, ): GitLabDiscoveryProcessor; // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, _optional: boolean, emit: CatalogProcessorEmit, ): Promise; - // (undocumented) - updateLastActivity(): Promise; } -// Warning: (ae-missing-release-tag) "inputError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) function inputError( atLocation: LocationSpec, message: string, ): CatalogProcessorResult; -// Warning: (ae-missing-release-tag) "location" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) function location_2( newLocation: LocationSpec, - optional: boolean, + optional?: boolean, ): CatalogProcessorResult; -// Warning: (ae-missing-release-tag) "LocationAnalyzer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type LocationAnalyzer = { analyzeLocation( @@ -894,12 +821,12 @@ export type LocationAnalyzer = { ): Promise; }; -// Warning: (ae-missing-release-tag) "LocationEntityProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class LocationEntityProcessor implements CatalogProcessor { constructor(options: LocationEntityProcessorOptions); // (undocumented) + getProcessorName(): string; + // (undocumented) postProcessEntity( entity: Entity, location: LocationSpec, @@ -907,20 +834,26 @@ export class LocationEntityProcessor implements CatalogProcessor { ): Promise; } -// Warning: (ae-missing-release-tag) "LocationEntityProcessorOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type LocationEntityProcessorOptions = { integrations: ScmIntegrationRegistry; }; -// Warning: (ae-missing-release-tag) "LocationService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export interface LocationInput { + // @deprecated (undocumented) + presence?: 'optional' | 'required'; + // (undocumented) + target: string; + // (undocumented) + type: string; +} + +// @public export interface LocationService { // (undocumented) createLocation( - spec: LocationSpec, + location: LocationInput, dryRun: boolean, options?: { authorizationToken?: string; @@ -950,12 +883,17 @@ export interface LocationService { }): Promise; } -// Warning: (ae-missing-release-tag) "LocationStore" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type LocationSpec = { + type: string; + target: string; + presence?: 'optional' | 'required'; +}; + +// @public export interface LocationStore { // (undocumented) - createLocation(spec: LocationSpec): Promise; + createLocation(location: LocationInput): Promise; // (undocumented) deleteLocation(id: string): Promise; // (undocumented) @@ -964,16 +902,12 @@ export interface LocationStore { listLocations(): Promise; } -// Warning: (ae-missing-release-tag) "notFoundError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) function notFoundError( atLocation: LocationSpec, message: string, ): CatalogProcessorResult; -// Warning: (ae-missing-release-tag) "PageInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type PageInfo = | { @@ -984,15 +918,13 @@ export type PageInfo = endCursor: string; }; -// Warning: (ae-missing-release-tag) "parseEntityYaml" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function parseEntityYaml( data: Buffer, location: LocationSpec, ): Iterable; -// @public +// @alpha export const permissionRules: { hasAnnotation: PermissionRule< Entity, @@ -1018,17 +950,15 @@ export const permissionRules: { >; }; -// Warning: (ae-missing-release-tag) "PlaceholderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class PlaceholderProcessor implements CatalogProcessor { constructor(options: PlaceholderProcessorOptions); // (undocumented) + getProcessorName(): string; + // (undocumented) preProcessEntity(entity: Entity, location: LocationSpec): Promise; } -// Warning: (ae-missing-release-tag) "PlaceholderProcessorOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type PlaceholderProcessorOptions = { resolvers: Record; @@ -1036,15 +966,11 @@ export type PlaceholderProcessorOptions = { integrations: ScmIntegrationRegistry; }; -// Warning: (ae-missing-release-tag) "PlaceholderResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type PlaceholderResolver = ( params: PlaceholderResolverParams, ) => Promise; -// Warning: (ae-missing-release-tag) "PlaceholderResolverParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type PlaceholderResolverParams = { key: string; @@ -1054,21 +980,40 @@ export type PlaceholderResolverParams = { resolveUrl: PlaceholderResolverResolveUrl; }; -// Warning: (ae-missing-release-tag) "PlaceholderResolverRead" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type PlaceholderResolverRead = (url: string) => Promise; -// Warning: (ae-missing-release-tag) "PlaceholderResolverResolveUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type PlaceholderResolverResolveUrl = ( url: string, base: string, ) => string; -// Warning: (ae-missing-release-tag) "RecursivePartial" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export const processingResult: Readonly<{ + readonly notFoundError: ( + atLocation: LocationSpec, + message: string, + ) => CatalogProcessorResult; + readonly inputError: ( + atLocation: LocationSpec, + message: string, + ) => CatalogProcessorResult; + readonly generalError: ( + atLocation: LocationSpec, + message: string, + ) => CatalogProcessorResult; + readonly location: ( + newLocation: LocationSpec, + optional?: boolean | undefined, + ) => CatalogProcessorResult; + readonly entity: ( + atLocation: LocationSpec, + newEntity: Entity, + ) => CatalogProcessorResult; + readonly relation: (spec: EntityRelationSpec) => CatalogProcessorResult; +}>; + // @public export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] @@ -1078,8 +1023,6 @@ export type RecursivePartial = { : T[P]; }; -// Warning: (ae-missing-release-tag) "RefreshIntervalFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type RefreshIntervalFunction = () => number; @@ -1094,9 +1037,7 @@ export interface RefreshService { refresh(options: RefreshOptions): Promise; } -// Warning: (ae-missing-release-tag) "relation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) function relation(spec: EntityRelationSpec): CatalogProcessorResult; declare namespace results { @@ -1129,14 +1070,10 @@ export interface RouterOptions { refreshService?: RefreshService; } -// Warning: (ae-missing-release-tag) "runPeriodically" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @public @deprecated export function runPeriodically(fn: () => any, delayMs: number): () => void; -// Warning: (ae-missing-release-tag) "StaticLocationProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export class StaticLocationProcessor implements StaticLocationProcessor { constructor(staticLocations: LocationSpec[]); // (undocumented) @@ -1149,12 +1086,9 @@ export class StaticLocationProcessor implements StaticLocationProcessor { ): Promise; } -// Warning: (ae-missing-release-tag) "UrlReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class UrlReaderProcessor implements CatalogProcessor { - // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts - constructor(options: Options_2); + constructor(options: { reader: UrlReader; logger: Logger_2 }); // (undocumented) getProcessorName(): string; // (undocumented) @@ -1166,8 +1100,4 @@ export class UrlReaderProcessor implements CatalogProcessor { cache: CatalogProcessorCache, ): Promise; } - -// Warnings were encountered during analysis: -// -// src/ingestion/processors/GithubMultiOrgReaderProcessor.d.ts:25:9 - (ae-forgotten-export) The symbol "GithubMultiOrgConfig" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 5701c9df48..957f18c15f 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; - export interface Config { /** * Configuration options for the catalog plugin. @@ -112,39 +110,6 @@ export interface Config { * List of processor-specific options and attributes */ processors?: { - /** - * GithubOrgReaderProcessor configuration - * - * @deprecated Configure a GitHub integration instead. - */ - githubOrg?: { - /** - * The configuration parameters for each single GitHub org provider. - */ - providers: Array<{ - /** - * The prefix of the target that this matches on, e.g. - * "https://github.com", with no trailing slash. - */ - target: string; - /** - * The base URL of the API of this provider, e.g. - * "https://api.github.com", with no trailing slash. - * - * May be omitted specifically for GitHub; then it will be deduced. - */ - apiBaseUrl?: string; - /** - * The authorization token to use for requests to this provider. - * - * If no token is specified, anonymous access is used. - * - * @visibility secret - */ - token?: string; - }>; - }; - /** * GithubMultiOrgReaderProcessor configuration */ @@ -172,19 +137,6 @@ export interface Config { userNamespace?: string; }>; }; - - /** - * AwsOrganizationCloudAccountProcessor configuration - */ - awsOrganization?: { - provider: { - /** - * The role to be assumed by this processor - * - */ - roleArn?: string; - }; - }; }; }; } diff --git a/plugins/airbrake/src/EntityAirbrakeContent.test.tsx b/plugins/catalog-backend/migrations/20220222164811_reprocess_for_relation_refs.js similarity index 61% rename from plugins/airbrake/src/EntityAirbrakeContent.test.tsx rename to plugins/catalog-backend/migrations/20220222164811_reprocess_for_relation_refs.js index f0afef425f..f4be2172a0 100644 --- a/plugins/airbrake/src/EntityAirbrakeContent.test.tsx +++ b/plugins/catalog-backend/migrations/20220222164811_reprocess_for_relation_refs.js @@ -13,13 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { EntityAirbrakeContent } from './plugin'; -import { renderInTestApp } from '@backstage/test-utils'; -describe('The Airbrake entity', () => { - it('should render the content properly', async () => { - const rendered = await renderInTestApp(); - expect(rendered.getByText('ChunkLoadError')).toBeInTheDocument(); +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // Make sure to reprocess everything, to make sure that relations have a targetRef produced + await knex('refresh_state').update({ + result_hash: '', + next_update_at: knex.fn.now(), }); -}); + await knex('final_entities').update({ hash: '' }); +}; + +exports.down = async function down() {}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index a4a40af206..f7d946c531 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.3", + "version": "0.22.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,7 +9,11 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" + }, + "backstage": { + "role": "backend-plugin" }, "homepage": "https://backstage.io", "repository": { @@ -21,26 +25,27 @@ "backstage" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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.2", - "@backstage/plugin-permission-common": "^0.4.0", - "@backstage/plugin-permission-node": "^0.4.3", - "@backstage/search-common": "^0.2.2", - "@backstage/types": "^0.1.1", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/plugin-catalog-common": "^0.1.4", + "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-permission-node": "^0.5.2", + "@backstage/plugin-scaffolder-common": "^0.2.2", + "@backstage/search-common": "^0.2.4", + "@backstage/types": "^0.1.3", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", "aws-sdk": "^2.840.0", @@ -65,10 +70,11 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.17", - "@backstage/cli": "^0.13.2", - "@backstage/plugin-permission-common": "^0.4.0", - "@backstage/test-utils": "^0.2.4", + "@backstage/backend-test-utils": "^0.1.19", + "@backstage/cli": "^0.14.1", + "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-search-backend-node": "0.4.7", + "@backstage/test-utils": "^0.2.6", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", @@ -83,6 +89,7 @@ }, "files": [ "dist", + "alpha", "migrations/**/*.{js,d.ts}", "config.d.ts" ], diff --git a/plugins/catalog-backend/src/api/common.ts b/plugins/catalog-backend/src/api/common.ts new file mode 100644 index 0000000000..1fd09aec32 --- /dev/null +++ b/plugins/catalog-backend/src/api/common.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityName } from '@backstage/catalog-model'; + +/** + * Holds the entity location information. + * + * @remarks + * + * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch. + * This flag is then set to indicate that the file can be not present. + * default value: 'required'. + * + * @public + */ +export type LocationSpec = { + type: string; + target: string; + presence?: 'optional' | 'required'; +}; + +/** + * Holds the relation data for entities. + * + * @public + */ +export type EntityRelationSpec = { + /** + * The source entity of this relation. + */ + source: EntityName; + + /** + * The type of the relation. + */ + type: string; + + /** + * The target entity of this relation. + */ + target: EntityName; +}; diff --git a/plugins/catalog-backend/src/ingestion/processors/results.ts b/plugins/catalog-backend/src/api/deprecatedResult.ts similarity index 66% rename from plugins/catalog-backend/src/ingestion/processors/results.ts rename to plugins/catalog-backend/src/api/deprecatedResult.ts index 5b6f07bae4..ecacc62c65 100644 --- a/plugins/catalog-backend/src/ingestion/processors/results.ts +++ b/plugins/catalog-backend/src/api/deprecatedResult.ts @@ -15,13 +15,16 @@ */ import { InputError, NotFoundError } from '@backstage/errors'; -import { - Entity, - EntityRelationSpec, - LocationSpec, -} from '@backstage/catalog-model'; -import { CatalogProcessorResult } from './types'; +import { Entity } from '@backstage/catalog-model'; +import { CatalogProcessorResult } from './processor'; +import { EntityRelationSpec, LocationSpec } from './common'; +// NOTE: This entire file is deprecated and should be eventually removed along with the `result` export + +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function notFoundError( atLocation: LocationSpec, message: string, @@ -33,6 +36,10 @@ export function notFoundError( }; } +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function inputError( atLocation: LocationSpec, message: string, @@ -44,6 +51,10 @@ export function inputError( }; } +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function generalError( atLocation: LocationSpec, message: string, @@ -51,13 +62,21 @@ export function generalError( return { type: 'error', location: atLocation, error: new Error(message) }; } +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function location( newLocation: LocationSpec, - optional: boolean, + optional?: boolean, ): CatalogProcessorResult { return { type: 'location', location: newLocation, optional }; } +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function entity( atLocation: LocationSpec, newEntity: Entity, @@ -65,6 +84,10 @@ export function entity( return { type: 'entity', location: atLocation, entity: newEntity }; } +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function relation(spec: EntityRelationSpec): CatalogProcessorResult { return { type: 'relation', relation: spec }; } diff --git a/plugins/catalog-backend/src/api/index.ts b/plugins/catalog-backend/src/api/index.ts new file mode 100644 index 0000000000..3f1c627743 --- /dev/null +++ b/plugins/catalog-backend/src/api/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as results from './deprecatedResult'; + +export { results }; + +export { processingResult } from './processingResult'; +export type { EntityRelationSpec, LocationSpec } from './common'; +export type { + CatalogProcessor, + CatalogProcessorParser, + CatalogProcessorCache, + CatalogProcessorEmit, + CatalogProcessorLocationResult, + CatalogProcessorEntityResult, + CatalogProcessorRelationResult, + CatalogProcessorErrorResult, + CatalogProcessorResult, +} from './processor'; +export type { + EntityProvider, + EntityProviderConnection, + EntityProviderMutation, +} from './provider'; diff --git a/plugins/catalog-backend/src/api/processingResult.ts b/plugins/catalog-backend/src/api/processingResult.ts new file mode 100644 index 0000000000..5ad908dc8b --- /dev/null +++ b/plugins/catalog-backend/src/api/processingResult.ts @@ -0,0 +1,71 @@ +/* + * 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 { InputError, NotFoundError } from '@backstage/errors'; +import { Entity } from '@backstage/catalog-model'; +import { CatalogProcessorResult } from './processor'; +import { EntityRelationSpec, LocationSpec } from './common'; + +/** + * Factory functions for the standard processing result types. + * + * @public + */ +export const processingResult = Object.freeze({ + notFoundError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { + type: 'error', + location: atLocation, + error: new NotFoundError(message), + }; + }, + + inputError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { + type: 'error', + location: atLocation, + error: new InputError(message), + }; + }, + + generalError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { type: 'error', location: atLocation, error: new Error(message) }; + }, + + location( + newLocation: LocationSpec, + optional?: boolean, + ): CatalogProcessorResult { + return { type: 'location', location: newLocation, optional }; + }, + + entity(atLocation: LocationSpec, newEntity: Entity): CatalogProcessorResult { + return { type: 'entity', location: atLocation, entity: newEntity }; + }, + + relation(spec: EntityRelationSpec): CatalogProcessorResult { + return { type: 'relation', relation: spec }; + }, +} as const); diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/api/processor.ts similarity index 90% rename from plugins/catalog-backend/src/ingestion/processors/types.ts rename to plugins/catalog-backend/src/api/processor.ts index 3e4e24b0fe..12a008edc7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/api/processor.ts @@ -14,19 +14,18 @@ * limitations under the License. */ -import { - Entity, - EntityRelationSpec, - LocationSpec, -} from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; +import { EntityRelationSpec, LocationSpec } from './common'; +/** + * @public + */ export type CatalogProcessor = { /** * A unique identifier for the Catalog Processor. - * It's strongly recommended to implement getProcessorName as this method will be required in the future. */ - getProcessorName?(): string; + getProcessorName(): string; /** * Reads the contents of a location. @@ -100,26 +99,13 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; - - /** - * Handles an emitted error. - * - * @param error - The error - * @param location - The location where the error occurred - * @param emit - A sink for items resulting from this handling - * @returns Nothing - */ - handleError?( - error: Error, - location: LocationSpec, - emit: CatalogProcessorEmit, - ): Promise; }; /** * A parser, that is able to take the raw catalog descriptor data and turn it * into the actual result pieces. The default implementation performs a YAML * document parsing. + * @public */ export type CatalogProcessorParser = (options: { data: Buffer; @@ -154,32 +140,38 @@ export interface CatalogProcessorCache { set(key: string, value: ItemType): Promise; } +/** @public */ export type CatalogProcessorEmit = (generated: CatalogProcessorResult) => void; +/** @public */ export type CatalogProcessorLocationResult = { type: 'location'; location: LocationSpec; - optional: boolean; + /** @deprecated Set `location.presence = 'optional'` instead */ + optional?: boolean; }; +/** @public */ export type CatalogProcessorEntityResult = { type: 'entity'; entity: Entity; location: LocationSpec; }; +/** @public */ export type CatalogProcessorRelationResult = { type: 'relation'; relation: EntityRelationSpec; - entityRef?: string; }; +/** @public */ export type CatalogProcessorErrorResult = { type: 'error'; error: Error; location: LocationSpec; }; +/** @public */ export type CatalogProcessorResult = | CatalogProcessorLocationResult | CatalogProcessorEntityResult diff --git a/plugins/catalog-backend/src/providers/types.ts b/plugins/catalog-backend/src/api/provider.ts similarity index 55% rename from plugins/catalog-backend/src/providers/types.ts rename to plugins/catalog-backend/src/api/provider.ts index 90c5f13215..fa7659ee32 100644 --- a/plugins/catalog-backend/src/providers/types.ts +++ b/plugins/catalog-backend/src/api/provider.ts @@ -16,15 +16,36 @@ import { DeferredEntity } from '../processing'; +/** + * @public + * A 'full' mutation replaces all existing entities created by this entity provider with new ones. + * A 'delta' mutation can both add and remove entities provided by this provider. Previously provided + * entities from a 'full' mutation are not removed. + */ export type EntityProviderMutation = | { type: 'full'; entities: DeferredEntity[] } | { type: 'delta'; added: DeferredEntity[]; removed: DeferredEntity[] }; +/** + * The EntityProviderConnection is the connection between the catalog and the entity provider. + * The EntityProvider use this connection to add and remove entities from the catalog. + * @public + */ export interface EntityProviderConnection { + /** + * Applies either a full or delta update to the catalog engine. + */ applyMutation(mutation: EntityProviderMutation): Promise; } +/** + * An EntityProvider is able to provide entities to the catalog. + * See https://backstage.io/docs/features/software-catalog/life-of-an-entity for more details. + * @public + */ export interface EntityProvider { + /** Unique provider name used internally for caching. */ getProviderName(): string; + /** Connect is called upon initialization by the catalog engine. */ connect(connection: EntityProviderConnection): Promise; } diff --git a/plugins/catalog-backend/src/catalog/index.ts b/plugins/catalog-backend/src/catalog/index.ts index 95c6572b10..59f67b2f52 100644 --- a/plugins/catalog-backend/src/catalog/index.ts +++ b/plugins/catalog-backend/src/catalog/index.ts @@ -18,9 +18,11 @@ export type { EntitiesCatalog, EntitiesRequest, EntitiesResponse, - EntityAncestryResponse, - PageInfo, EntitiesSearchFilter, + EntityAncestryResponse, + EntityFacetsRequest, + EntityFacetsResponse, EntityFilter, EntityPagination, + PageInfo, } from './types'; diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index a79229b9a1..b575b96c6c 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -21,6 +21,7 @@ import { Entity } from '@backstage/catalog-model'; * * Any (at least one) of the outer sets must match, within which all of the * individual filters must match. + * @public */ export type EntityFilter = | { allOf: EntityFilter[] } @@ -30,6 +31,7 @@ export type EntityFilter = /** * A pagination rule for entities. + * @public */ export type EntityPagination = { limit?: number; @@ -38,7 +40,8 @@ export type EntityPagination = { }; /** - * Matches rows in the entities_search table. + * Matches rows in the search table. + * @public */ export type EntitiesSearchFilter = { /** @@ -57,6 +60,7 @@ export type EntitiesSearchFilter = { values?: string[]; }; +/** @public */ export type PageInfo = | { hasNextPage: false; @@ -66,6 +70,7 @@ export type PageInfo = endCursor: string; }; +/** @public */ export type EntitiesRequest = { filter?: EntityFilter; fields?: (entity: Entity) => Entity; @@ -73,6 +78,7 @@ export type EntitiesRequest = { authorizationToken?: string; }; +/** @public */ export type EntitiesResponse = { entities: Entity[]; pageInfo: PageInfo; @@ -87,6 +93,44 @@ export type EntityAncestryResponse = { }>; }; +/** + * The request shape for {@link EntitiesCatalog.facets}. + * + * @public + */ +export interface EntityFacetsRequest { + /** + * A filter to apply on the full list of entities before computing the facets. + */ + filter?: EntityFilter; + /** + * The facets to compute. + * + * @remarks + * + * This is a list of strings corresponding to paths within individual entity + * shapes. For example, to compute the facets for all available tags, you + * would pass in the string 'metadata.tags'. + */ + facets: string[]; + /** + * The optional token that authorizes the action. + */ + authorizationToken?: string; +} + +/** + * The response shape for {@link EntitiesCatalog.facets}. + * + * @public + */ +export interface EntityFacetsResponse { + /** + * The computed facets, one entry per facet in the request. + */ + facets: Record>; +} + /** @public */ export type EntitiesCatalog = { /** @@ -115,4 +159,12 @@ export type EntitiesCatalog = { entityRef: string, options?: { authorizationToken?: string }, ): Promise; + + /** + * Computes facets for a set of entities, e.g. for populating filter lists + * or driving insights or similar. + * + * @param request - Request options + */ + facets(request: EntityFacetsRequest): Promise; }; diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index ac2ff07498..1affb14b45 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import { Entity, EntityRelationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; import { DateTime } from 'luxon'; +import { EntityRelationSpec } from '../api'; import { DeferredEntity } from '../processing/types'; /** diff --git a/plugins/catalog-backend/src/index.ts b/plugins/catalog-backend/src/index.ts index efdaa4b8c5..4c1e753300 100644 --- a/plugins/catalog-backend/src/index.ts +++ b/plugins/catalog-backend/src/index.ts @@ -20,11 +20,12 @@ * @packageDocumentation */ +export * from './api'; export * from './catalog'; export * from './ingestion'; +export * from './modules'; export * from './search'; export * from './util'; export * from './processing'; -export * from './providers'; export * from './service'; export * from './permissions'; diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index 24d6b2fd66..13430f51dd 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { DefaultCatalogRulesEnforcer } from './CatalogRules'; +import { LocationSpec } from '../api'; const entity = { user: { @@ -35,11 +36,11 @@ const entity = { const location: Record = { x: { - type: 'github', + type: 'url', target: 'https://github.com/a/b/blob/master/x.yaml', }, y: { - type: 'github', + type: 'url', target: 'https://github.com/a/b/blob/master/y.yaml', }, z: { @@ -180,7 +181,7 @@ describe('DefaultCatalogRulesEnforcer', () => { rules: [{ allow: ['user'] }], locations: [ { - type: 'github', + type: 'url', target: 'https://github.com/a/b/blob/master/x.yaml', rules: [ { @@ -204,7 +205,7 @@ describe('DefaultCatalogRulesEnforcer', () => { const enforcer = DefaultCatalogRulesEnforcer.fromConfig( new ConfigReader({ catalog: { - rules: [{ allow: ['Group'], locations: [{ type: 'github' }] }], + rules: [{ allow: ['Group'], locations: [{ type: 'url' }] }], }, }), ); diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index a0ef5f3d53..e4bfdb8d0f 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -15,8 +15,9 @@ */ import { Config } from '@backstage/config'; -import { LocationSpec, Entity } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import path from 'path'; +import { LocationSpec } from '../api'; /** * Rules to apply to catalog entities. diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index fcb634914b..1f4729b041 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -16,8 +16,6 @@ export { DefaultCatalogRulesEnforcer } from './CatalogRules'; export type { CatalogRule, CatalogRulesEnforcer } from './CatalogRules'; -export * from './processors'; -export * from './providers'; export type { AnalyzeLocationEntityField, AnalyzeLocationExistingEntity, diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.ts b/plugins/catalog-backend/src/ingestion/processors/github/config.ts deleted file mode 100644 index 27bec6eb0d..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/github/config.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Config } from '@backstage/config'; -import { trimEnd } from 'lodash'; - -/** - * The configuration parameters for a single GitHub API provider. - */ -export type ProviderConfig = { - /** - * The prefix of the target that this matches on, e.g. "https://github.com", - * with no trailing slash. - */ - target: string; - - /** - * The base URL of the API of this provider, e.g. "https://api.github.com", - * with no trailing slash. - * - * May be omitted specifically for GitHub; then it will be deduced. - */ - apiBaseUrl?: string; - - /** - * The authorization token to use for requests to this provider. - * - * If no token is specified, anonymous access is used. - */ - token?: string; -}; - -// TODO(freben): Break out common code and config from here and GithubReaderProcessor -export function readGithubConfig(config: Config): ProviderConfig[] { - const providers: ProviderConfig[] = []; - - const providerConfigs = - config.getOptionalConfigArray('catalog.processors.githubOrg.providers') ?? - []; - - // First read all the explicit providers - for (const providerConfig of providerConfigs) { - const target = trimEnd(providerConfig.getString('target'), '/'); - let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl'); - const token = providerConfig.getOptionalString('token'); - - if (apiBaseUrl) { - apiBaseUrl = trimEnd(apiBaseUrl, '/'); - } else if (target === 'https://github.com') { - apiBaseUrl = 'https://api.github.com'; - } - - if (!apiBaseUrl) { - throw new Error( - `Provider at ${target} must configure an explicit apiBaseUrl`, - ); - } - - providers.push({ target, apiBaseUrl, token }); - } - - // If no explicit github.com provider was added, put one in the list as - // a convenience - if (!providers.some(p => p.target === 'https://github.com')) { - providers.push({ - target: 'https://github.com', - apiBaseUrl: 'https://api.github.com', - }); - } - - return providers; -} - -/** - * The configuration parameters for a multi-org GitHub processor. - */ -export type GithubMultiOrgConfig = Array<{ - /** - * The name of the GitHub org to process. - */ - name: string; - /** - * The namespace of the group created for this org. - */ - groupNamespace: string; - /** - * The namespace of the users created for this org. If not specified defaults to undefined. - */ - userNamespace: string | undefined; -}>; - -export function readGithubMultiOrgConfig(config: Config): GithubMultiOrgConfig { - const orgConfigs = config.getOptionalConfigArray('orgs') ?? []; - return orgConfigs.map(c => ({ - name: c.getString('name'), - groupNamespace: ( - c.getOptionalString('groupNamespace') ?? c.getString('name') - ).toLowerCase(), - userNamespace: c.getOptionalString('userNamespace') ?? undefined, - })); -} diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index 30cdee4760..37d12d12b1 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { RecursivePartial } from '../util/RecursivePartial'; +import { LocationSpec } from '../api'; -// -// LocationAnalyzer -// - +/** @public */ export type LocationAnalyzer = { /** * Generates an entity configuration for given git repository. It's used for @@ -33,30 +31,38 @@ export type LocationAnalyzer = { ): Promise; }; +/** @public */ export type AnalyzeLocationRequest = { location: LocationSpec; }; +/** @public */ export type AnalyzeLocationResponse = { existingEntityFiles: AnalyzeLocationExistingEntity[]; generateEntities: AnalyzeLocationGenerateEntity[]; }; -// If the folder pointed to already contained catalog info yaml files, they are -// read and emitted like this so that the frontend can inform the user that it -// located them and can make sure to register them as well if they weren't -// already +/** + * If the folder pointed to already contained catalog info yaml files, they are + * read and emitted like this so that the frontend can inform the user that it + * located them and can make sure to register them as well if they weren't + * already + * @public + */ export type AnalyzeLocationExistingEntity = { location: LocationSpec; isRegistered: boolean; entity: Entity; }; -// This is some form of representation of what the analyzer could deduce. -// We should probably have a chat about how this can best be conveyed to -// the frontend. It'll probably contain a (possibly incomplete) entity, plus -// enough info for the frontend to know what form data to show to the user -// for overriding/completing the info. +/** + * This is some form of representation of what the analyzer could deduce. + * We should probably have a chat about how this can best be conveyed to + * the frontend. It'll probably contain a (possibly incomplete) entity, plus + * enough info for the frontend to know what form data to show to the user + * for overriding/completing the info. + * @public + * */ export type AnalyzeLocationGenerateEntity = { // Some form of partial representation of the entity entity: RecursivePartial; @@ -67,12 +73,15 @@ export type AnalyzeLocationGenerateEntity = { // This is where I get really vague. Something like this perhaps? Or it could be // something like a json-schema that contains enough info for the frontend to // be able to present a form and explanations +/** @public */ export type AnalyzeLocationEntityField = { - // e.g. "spec.owner"? The frontend needs to know how to "inject" the field into the - // entity again if the user wants to change it + /** + * e.g. "spec.owner"? The frontend needs to know how to "inject" the field into the + * entity again if the user wants to change it + */ field: string; - // The outcome of the analysis for this particular field + /** The outcome of the analysis for this particular field */ state: | 'analysisSuggestedValue' | 'analysisSuggestedNoValue' @@ -81,10 +90,11 @@ export type AnalyzeLocationEntityField = { // If the analysis did suggest a value, this is where it would be. Not sure if we want // to limit this to strings or if we want it to be any JsonValue value: string | null; - - // A text to show to the user to inform about the choices made. Like, it could say - // "Found a CODEOWNERS file that covers this target, so we suggest leaving this - // field empty; which would currently make it owned by X" where X is taken from the - // codeowners file. + /** + * A text to show to the user to inform about the choices made. Like, it could say + * "Found a CODEOWNERS file that covers this target, so we suggest leaving this + * field empty; which would currently make it owned by X" where X is taken from the + * codeowners file. + */ description: string; }; diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.test.ts similarity index 89% rename from plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.test.ts index 064a2ad724..249de71591 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.test.ts @@ -13,11 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger, UrlReaders } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; -import { CatalogProcessorEntityResult, CatalogProcessorResult } from './types'; -import { defaultEntityDataParser } from './util/parse'; +import { + CatalogProcessorEntityResult, + CatalogProcessorResult, +} from '../../api'; +import { defaultEntityDataParser } from '../util/parse'; import AWSMock from 'aws-sdk-mock'; import aws from 'aws-sdk'; import path from 'path'; @@ -36,10 +40,7 @@ AWSMock.mock( 'getObject', Buffer.from( require('fs').readFileSync( - path.resolve( - __dirname, - '__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt', - ), + path.resolve(__dirname, '__fixtures__/awsS3-mock-object.txt'), ), ), ); diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.ts similarity index 88% rename from plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.ts index 63bc45726d..737f8f7c2f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.ts @@ -15,19 +15,24 @@ */ import { UrlReader } from '@backstage/backend-common'; -import { LocationSpec } from '@backstage/catalog-model'; import { isError } from '@backstage/errors'; import limiterFactory from 'p-limit'; -import * as result from './results'; import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, -} from './types'; + LocationSpec, + processingResult, +} from '../../api'; +/** @public */ export class AwsS3DiscoveryProcessor implements CatalogProcessor { constructor(private readonly reader: UrlReader) {} + getProcessorName(): string { + return 'AwsS3DiscoveryProcessor'; + } + async readLocation( location: LocationSpec, optional: boolean, @@ -53,10 +58,10 @@ export class AwsS3DiscoveryProcessor implements CatalogProcessor { if (isError(error) && error.name === 'NotFoundError') { if (!optional) { - emit(result.notFoundError(location, message)); + emit(processingResult.notFoundError(location, message)); } } else { - emit(result.generalError(location, message)); + emit(processingResult.generalError(location, message)); } } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt b/plugins/catalog-backend/src/modules/aws/__fixtures__/awsS3-mock-object.txt similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt rename to plugins/catalog-backend/src/modules/aws/__fixtures__/awsS3-mock-object.txt diff --git a/plugins/catalog-backend/src/modules/aws/index.ts b/plugins/catalog-backend/src/modules/aws/index.ts new file mode 100644 index 0000000000..9477d4104d --- /dev/null +++ b/plugins/catalog-backend/src/modules/aws/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 { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.test.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.test.ts index 463d982ca2..450a0482ed 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.test.ts @@ -15,15 +15,15 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { LocationSpec } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; -import { codeSearch } from './azure'; +import { codeSearch } from './lib'; import { AzureDevOpsDiscoveryProcessor, parseUrl, } from './AzureDevOpsDiscoveryProcessor'; +import { LocationSpec } from '../../api'; -jest.mock('./azure'); +jest.mock('./lib'); const mockCodeSearch = codeSearch as jest.MockedFunction; describe('AzureDevOpsDiscoveryProcessor', () => { @@ -167,7 +167,6 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml', presence: 'optional', }, - optional: true, }); expect(emitter).toHaveBeenCalledWith({ type: 'location', @@ -177,7 +176,6 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'https://dev.azure.com/shopify/engineering/_git/ios-app?path=/src/catalog-info.yaml', presence: 'optional', }, - optional: true, }); }); @@ -215,7 +213,6 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml', presence: 'optional', }, - optional: true, }); }); @@ -254,7 +251,6 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/src/main/catalog.yaml', presence: 'optional', }, - optional: true, }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.ts similarity index 84% rename from plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.ts index aeac64f54d..8e36e810b8 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.ts @@ -14,13 +14,19 @@ * limitations under the License. */ -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'; -import { codeSearch } from './azure'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { codeSearch } from './lib'; /** * Extracts repositories out of an Azure DevOps org. @@ -35,9 +41,11 @@ import { codeSearch } from './azure'; * You may also explicitly specify a single repo: * * target: https://dev.azure.com/org/project/_git/repo + * + * @public **/ 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,11 +57,18 @@ 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; } + getProcessorName(): string { + return 'AzureDevOpsDiscoveryProcessor'; + } + async readLocation( location: LocationSpec, _optional: boolean, @@ -91,17 +106,14 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { for (const file of files) { emit( - results.location( - { - type: 'url', - target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`, - presence: 'optional', - }, + processingResult.location({ + type: 'url', + target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`, // Not all locations may actually exist, since the user defined them as a wildcard pattern. // Thus, we emit them as optional and let the downstream processor find them while not outputting // an error if it couldn't. - true, - ), + presence: 'optional', + }), ); } diff --git a/plugins/catalog-backend/src/modules/azure/index.ts b/plugins/catalog-backend/src/modules/azure/index.ts new file mode 100644 index 0000000000..9c76336804 --- /dev/null +++ b/plugins/catalog-backend/src/modules/azure/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 { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts b/plugins/catalog-backend/src/modules/azure/lib/azure.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts rename to plugins/catalog-backend/src/modules/azure/lib/azure.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts b/plugins/catalog-backend/src/modules/azure/lib/azure.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/azure/azure.ts rename to plugins/catalog-backend/src/modules/azure/lib/azure.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/index.ts b/plugins/catalog-backend/src/modules/azure/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/azure/index.ts rename to plugins/catalog-backend/src/modules/azure/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.test.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.test.ts index 7ace995fda..857f999a70 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.test.ts @@ -13,19 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger } from '@backstage/backend-common'; import { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; import { ConfigReader } from '@backstage/config'; -import { LocationSpec } from '@backstage/catalog-model'; -import { - BitbucketRepository20, - BitbucketRepositoryParser, - PagedResponse, - PagedResponse20, -} from './bitbucket'; -import { results } from './index'; import { RequestHandler, rest } from 'msw'; import { setupServer } from 'msw/node'; +import { BitbucketRepository20, PagedResponse, PagedResponse20 } from './lib'; +import { LocationSpec, processingResult } from '../../api'; const server = setupServer(); @@ -210,7 +205,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/projects/backstage/repos/backstage/browse/catalog.yaml', presence: 'optional', }, - optional: true, }); expect(emitter).toHaveBeenCalledWith({ type: 'location', @@ -220,7 +214,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -246,7 +239,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -269,7 +261,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse/catalog.yaml?ref=branch-name', presence: 'optional', }, - optional: true, }); }); @@ -292,7 +283,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/projects/backstage/repos/test/browse/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -321,7 +311,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse/catalog-info.yaml', presence: 'optional', }, - optional: true, }); }); }); @@ -369,7 +358,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/backstage/browse/catalog.yaml', presence: 'optional', }, - optional: true, }); expect(emitter).toHaveBeenCalledWith({ type: 'location', @@ -379,7 +367,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/custom-path/projects/demo/repos/demo/browse/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -408,7 +395,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/techdocs-cli/browse/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -434,7 +420,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/custom-path/projects/demo/repos/demo/browse/catalog.yaml?ref=branch-name', presence: 'optional', }, - optional: true, }); }); @@ -460,7 +445,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/test/browse/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -492,7 +476,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/techdocs-cli/browse/catalog-info.yaml', presence: 'optional', }, - optional: true, }); }); }); @@ -536,7 +519,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', presence: 'optional', }, - optional: true, }); expect(emitter).toHaveBeenCalledWith({ type: 'location', @@ -546,7 +528,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-two/src/master/catalog-info.yaml', presence: 'optional', }, - optional: true, }); }); @@ -574,7 +555,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-one/src/master/my/nested/path/catalog.yaml', presence: 'optional', }, - optional: true, }); expect(emitter).toHaveBeenCalledWith({ type: 'location', @@ -584,7 +564,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-two/src/master/my/nested/path/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -612,7 +591,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog.yaml', presence: 'optional', }, - optional: true, }); expect(emitter).toHaveBeenCalledWith({ type: 'location', @@ -622,7 +600,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-two/src/master/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -649,7 +626,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -677,7 +653,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-three/src/master/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -703,7 +678,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', presence: 'optional', }, - optional: true, }); expect(mockCall).toBeCalledTimes(1); // it should be possible to do this via an `expect.objectContaining` check but seems to fail with some encoding issue. @@ -739,7 +713,6 @@ describe('BitbucketDiscoveryProcessor', () => { 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', presence: 'optional', }, - optional: true, }); }); @@ -764,17 +737,6 @@ describe('BitbucketDiscoveryProcessor', () => { }); describe('Custom repository parser', () => { - const customRepositoryParser: BitbucketRepositoryParser = - async function* customRepositoryParser({}) { - yield results.location( - { - type: 'custom-location-type', - target: 'custom-target', - }, - true, - ); - }; - const processor = BitbucketDiscoveryProcessor.fromConfig( new ConfigReader({ integrations: { @@ -787,7 +749,16 @@ describe('BitbucketDiscoveryProcessor', () => { ], }, }), - { parser: customRepositoryParser, logger: getVoidLogger() }, + { + parser: async function* customRepositoryParser({}) { + yield processingResult.location({ + type: 'custom-location-type', + target: 'custom-target', + presence: 'optional', + }); + }, + logger: getVoidLogger(), + }, ); it('use custom repository parser', async () => { @@ -808,8 +779,8 @@ describe('BitbucketDiscoveryProcessor', () => { location: { type: 'custom-location-type', target: 'custom-target', + presence: 'optional', }, - optional: true, }); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.ts similarity index 91% rename from plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.ts index f08888d1ab..daf24e2427 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Logger } from 'winston'; import { Config } from '@backstage/config'; @@ -21,30 +22,45 @@ import { ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; -import { LocationSpec } from '@backstage/catalog-model'; import { - BitbucketRepositoryParser, BitbucketClient, defaultRepositoryParser, paginated, paginated20, BitbucketRepository, BitbucketRepository20, -} from './bitbucket'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; +} from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + CatalogProcessorResult, + LocationSpec, +} from '../../api'; const DEFAULT_BRANCH = 'master'; const DEFAULT_CATALOG_LOCATION = '/catalog-info.yaml'; const EMPTY_CATALOG_LOCATION = '/'; +/** @public */ export class BitbucketDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; - private readonly parser: BitbucketRepositoryParser; + private readonly parser: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger; + }) => AsyncIterable; private readonly logger: Logger; static fromConfig( config: Config, - options: { parser?: BitbucketRepositoryParser; logger: Logger }, + options: { + parser?: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger; + }) => AsyncIterable; + logger: Logger; + }, ) { const integrations = ScmIntegrations.fromConfig(config); @@ -56,7 +72,11 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - parser?: BitbucketRepositoryParser; + parser?: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger; + }) => AsyncIterable; logger: Logger; }) { this.integrations = options.integrations; @@ -64,6 +84,10 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { this.logger = options.logger; } + getProcessorName(): string { + return 'BitbucketDiscoveryProcessor'; + } + async readLocation( location: LocationSpec, _optional: boolean, diff --git a/plugins/catalog-backend/src/modules/bitbucket/index.ts b/plugins/catalog-backend/src/modules/bitbucket/index.ts new file mode 100644 index 0000000000..0e39083520 --- /dev/null +++ b/plugins/catalog-backend/src/modules/bitbucket/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 { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; +export type { BitbucketRepositoryParser } from './lib'; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.test.ts similarity index 74% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.test.ts index 397d259bae..9f2d8b2daa 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.test.ts @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { processingResult } from '../../../api'; import { defaultRepositoryParser } from './BitbucketRepositoryParser'; -import { results } from '../index'; -import { getVoidLogger } from '@backstage/backend-common'; -import { BitbucketIntegration } from '@backstage/integration'; describe('BitbucketRepositoryParser', () => { describe('defaultRepositoryParser', () => { @@ -25,19 +24,14 @@ describe('BitbucketRepositoryParser', () => { 'https://bitbucket.mycompany.com/projects/project-key/repos/repo-slug/browse'; const path = '/catalog-info.yaml'; const expected = [ - results.location( - { - type: 'url', - target: `${browseUrl}${path}`, - presence: 'optional', - }, - true, - ), + processingResult.location({ + type: 'url', + target: `${browseUrl}${path}`, + presence: 'optional', + }), ]; const actual = await defaultRepositoryParser({ - integration: {} as BitbucketIntegration, target: `${browseUrl}${path}`, - logger: getVoidLogger(), }); let i = 0; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.ts similarity index 75% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.ts index 34a9284b3e..c72e560c21 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.ts @@ -13,28 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CatalogProcessorResult } from '../types'; -import { results } from '../index'; -import { Logger } from 'winston'; -import { BitbucketIntegration } from '@backstage/integration'; +import { BitbucketIntegration } from '@backstage/integration'; +import { Logger } from 'winston'; +import { CatalogProcessorResult, processingResult } from '../../../api'; + +/** + * @public + * @deprecated type inlined. + */ export type BitbucketRepositoryParser = (options: { integration: BitbucketIntegration; target: string; logger: Logger; }) => AsyncIterable; -export const defaultRepositoryParser: BitbucketRepositoryParser = - async function* defaultRepositoryParser({ target }) { - yield results.location( - { - type: 'url', - target: target, - presence: 'optional', - }, +export const defaultRepositoryParser = + async function* defaultRepositoryParser(options: { target: string }) { + yield processingResult.location({ + type: 'url', + target: options.target, // Not all locations may actually exist, since the user defined them as a wildcard pattern. // Thus, we emit them as optional and let the downstream processor find them while not outputting // an error if it couldn't. - true, - ); + presence: 'optional', + }); }; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/client.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/client.ts index 0c132bb61b..65cd28d5ab 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/client.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import fetch from 'node-fetch'; +import fetch from 'node-fetch'; import { BitbucketIntegrationConfig, getBitbucketRequestOptions, diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/index.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/index.ts index 4b28c7f115..a819bb3c64 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { BitbucketClient, paginated, paginated20 } from './client'; export { defaultRepositoryParser } from './BitbucketRepositoryParser'; export type { PagedResponse, PagedResponse20 } from './client'; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/types.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/types.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts rename to plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts index 2f52a1ddd9..8cf250de2a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts @@ -15,9 +15,9 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { LocationSpec } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { CodeOwnersProcessor } from './CodeOwnersProcessor'; +import { LocationSpec } from '../../api'; const mockCodeOwnersText = () => ` * @acme/team-foo @acme/team-bar @@ -27,7 +27,7 @@ const mockCodeOwnersText = () => ` describe('CodeOwnersProcessor', () => { const mockLocation = ({ basePath = '', - type = 'github', + type = 'url', } = {}): LocationSpec => ({ type, target: `https://github.com/backstage/backstage/blob/master/${basePath}catalog-info.yaml`, diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts similarity index 82% rename from plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts rename to plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts index a70aa6d1a9..6f6aee7161 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts @@ -15,27 +15,22 @@ */ import { UrlReader } from '@backstage/backend-common'; -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Entity } 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'; +import { CatalogProcessor, LocationSpec } from '../../api'; +import { findCodeOwnerByTarget } from './lib'; const ALLOWED_KINDS = ['API', 'Component', 'Domain', 'Resource', 'System']; +const ALLOWED_LOCATION_TYPES = ['url']; -const ALLOWED_LOCATION_TYPES = [ - 'url', - 'azure/api', - 'bitbucket/api', - 'github', - 'github/api', - 'gitlab', - 'gitlab/api', -]; - +/** @public */ export class CodeOwnersProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; private readonly reader: UrlReader; @@ -52,7 +47,7 @@ export class CodeOwnersProcessor implements CatalogProcessor { } constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger; reader: UrlReader; }) { @@ -61,6 +56,10 @@ export class CodeOwnersProcessor implements CatalogProcessor { this.reader = options.reader; } + getProcessorName(): string { + return 'CodeOwnersProcessor'; + } + async preProcessEntity( entity: Entity, location: LocationSpec, diff --git a/plugins/catalog-backend/src/modules/codeowners/index.ts b/plugins/catalog-backend/src/modules/codeowners/index.ts new file mode 100644 index 0000000000..6b7569ff07 --- /dev/null +++ b/plugins/catalog-backend/src/modules/codeowners/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 { CodeOwnersProcessor } from './CodeOwnersProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/index.ts b/plugins/catalog-backend/src/modules/codeowners/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/index.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/read.test.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/read.test.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/read.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/read.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/read.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/read.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.test.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.test.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/scm.ts b/plugins/catalog-backend/src/modules/codeowners/lib/scm.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/scm.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/scm.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.test.ts index 41d44ab157..3911f7f743 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.test.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { LocationSpec } from '../../api'; import { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; describe('AnnotateLocationEntityProcessor', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts similarity index 68% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts index 54079e13ff..4af0b8cf61 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts @@ -15,25 +15,33 @@ */ 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'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; - -type Options = { - integrations: ScmIntegrationRegistry; -}; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, +} from '../../api'; +/** @public */ export class AnnotateLocationEntityProcessor implements CatalogProcessor { - constructor(private readonly options: Options) {} + constructor( + private readonly options: { + integrations: ScmIntegrationRegistry; + }, + ) {} + + getProcessorName(): string { + return 'AnnotateLocationEntityProcessor'; + } async preProcessEntity( entity: Entity, @@ -58,7 +66,7 @@ export class AnnotateLocationEntityProcessor implements CatalogProcessor { }); if (sourceUrl) { - sourceLocation = stringifyLocationReference({ + sourceLocation = stringifyLocationRef({ type: 'url', target: sourceUrl, }); @@ -70,12 +78,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/AnnotateScmSlugEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.test.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.test.ts index 5bdf4a4b83..1e553a44f8 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.test.ts @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; +import { LocationSpec } from '../../api'; describe('AnnotateScmSlugEntityProcessor', () => { describe('github', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts similarity index 91% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts index 483472da4b..e44db4d44d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { ScmIntegrationRegistry, @@ -21,15 +21,20 @@ import { } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; import { identity, merge, pickBy } from 'lodash'; -import { CatalogProcessor } from './types'; +import { CatalogProcessor, LocationSpec } from '../../api'; const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; +/** @public */ export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { constructor( private readonly opts: { scmIntegrationRegistry: ScmIntegrationRegistry }, ) {} + getProcessorName(): string { + return 'AnnotateScmSlugEntityProcessor'; + } + static fromConfig(config: Config): AnnotateScmSlugEntityProcessor { return new AnnotateScmSlugEntityProcessor({ scmIntegrationRegistry: ScmIntegrations.fromConfig(config), diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts index c42980d97b..2532293acf 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts @@ -21,9 +21,9 @@ import { GroupEntity, ResourceEntity, SystemEntity, - TemplateEntityV1beta2, UserEntity, } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; import { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; describe('BuiltinKindsEntityProcessor', () => { @@ -187,7 +187,7 @@ describe('BuiltinKindsEntityProcessor', () => { await expect( processor.postProcessEntity(entity, location, emit), ).rejects.toThrowError( - 'Entity reference "r" did not specify a kind (e.g. starting with "Component:"), and has no default', + 'Entity reference "r" had missing or empty kind (e.g. did not start with "component:" or similar)', ); }); @@ -360,7 +360,7 @@ describe('BuiltinKindsEntityProcessor', () => { await expect( processor.postProcessEntity(entity, location, emit), ).rejects.toThrowError( - 'Entity reference "c" did not specify a kind (e.g. starting with "Component:"), and has no default', + 'Entity reference "c" had missing or empty kind (e.g. did not start with "component:" or similar)', ); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts index 5999be9793..548cdf3115 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts @@ -26,7 +26,6 @@ import { GroupEntity, groupEntityV1alpha1Validator, locationEntityV1alpha1Validator, - LocationSpec, parseEntityRef, RELATION_API_CONSUMED_BY, RELATION_API_PROVIDED_BY, @@ -46,14 +45,21 @@ import { resourceEntityV1alpha1Validator, SystemEntity, systemEntityV1alpha1Validator, - TemplateEntityV1beta2, - templateEntityV1beta2Validator, UserEntity, userEntityV1alpha1Validator, } from '@backstage/catalog-model'; -import * as result from './results'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; +import { + TemplateEntityV1beta2, + templateEntityV1beta2Validator, +} from '@backstage/plugin-scaffolder-common'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +/** @public */ export class BuiltinKindsEntityProcessor implements CatalogProcessor { private readonly validators = [ apiEntityV1alpha1Validator, @@ -67,6 +73,10 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { domainEntityV1alpha1Validator, ]; + getProcessorName(): string { + return 'BuiltinKindsEntityProcessor'; + } + async validateEntityKind(entity: Entity): Promise { for (const validator of this.validators) { const results = await validator.check(entity); @@ -100,13 +110,8 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { } for (const target of [targets].flat()) { const targetRef = parseEntityRef(target, context); - if (targetRef.kind === undefined) { - throw new Error( - `Entity reference "${target}" did not specify a kind (e.g. starting with "Component:"), and has no default`, - ); - } emit( - result.relation({ + processingResult.relation({ source: selfRef, type: outgoingRelation, target: { @@ -117,7 +122,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { }), ); emit( - result.relation({ + processingResult.relation({ source: { kind: targetRef.kind, namespace: targetRef.namespace, diff --git a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.test.ts b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.test.ts similarity index 98% rename from plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.test.ts rename to plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.test.ts index 0663ad8d04..d61342e3d0 100644 --- a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.test.ts +++ b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.test.ts @@ -17,7 +17,7 @@ import { ConfigReader } from '@backstage/config'; import path from 'path'; import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider'; -import { EntityProviderConnection } from './types'; +import { EntityProviderConnection } from '../../api'; describe('ConfigLocationEntityProvider', () => { it('should apply mutation with the correct paths in the config', async () => { diff --git a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.ts b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.ts similarity index 90% rename from plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.ts rename to plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.ts index 389cd8d9ff..9529a5b608 100644 --- a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.ts +++ b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.ts @@ -16,9 +16,9 @@ import { Config } from '@backstage/config'; import path from 'path'; -import { getEntityLocationRef } from '../processing/util'; -import { EntityProvider, EntityProviderConnection } from './types'; -import { locationSpecToLocationEntity } from '../util/conversion'; +import { getEntityLocationRef } from '../../processing/util'; +import { EntityProvider, EntityProviderConnection } from '../../api'; +import { locationSpecToLocationEntity } from '../../util/conversion'; export class ConfigLocationEntityProvider implements EntityProvider { constructor(private readonly config: Config) {} diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts similarity index 98% rename from plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts rename to plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts index b9d129de5f..dfbc37f6c9 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts +++ b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts @@ -15,7 +15,7 @@ */ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { v4 as uuid } from 'uuid'; -import { applyDatabaseMigrations } from '../database/migrations'; +import { applyDatabaseMigrations } from '../../database/migrations'; import { DefaultLocationStore } from './DefaultLocationStore'; describe('DefaultLocationStore', () => { diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.ts similarity index 83% rename from plugins/catalog-backend/src/providers/DefaultLocationStore.ts rename to plugins/catalog-backend/src/modules/core/DefaultLocationStore.ts index 9214cd0bc1..d637f4eda0 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { Location, 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'; -import { DbLocationsRow } from '../database/tables'; -import { getEntityLocationRef } from '../processing/util'; -import { EntityProvider, EntityProviderConnection } from './types'; -import { locationSpecToLocationEntity } from '../util/conversion'; -import { LocationStore } from '../service'; +import { DbLocationsRow } from '../../database/tables'; +import { getEntityLocationRef } from '../../processing/util'; +import { EntityProvider, EntityProviderConnection } from '../../api'; +import { locationSpecToLocationEntity } from '../../util/conversion'; +import { LocationInput, LocationStore } from '../../service'; export class DefaultLocationStore implements LocationStore, EntityProvider { private _connection: EntityProviderConnection | undefined; @@ -33,25 +33,25 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { return 'DefaultLocationStore'; } - async createLocation(spec: LocationSpec): Promise { + async createLocation(input: LocationInput): Promise { const location = await this.db.transaction(async tx => { - // Attempt to find a previous location matching the spec + // Attempt to find a previous location matching the input const previousLocations = await this.locations(tx); - // TODO: when location id's are a compilation of spec target we can remove this full + // TODO: when location id's are a compilation of input target we can remove this full // lookup of locations first and just grab the by that instead. const previousLocation = previousLocations.some( - l => spec.type === l.type && spec.target === l.target, + l => input.type === l.type && input.target === l.target, ); if (previousLocation) { throw new ConflictError( - `Location ${spec.type}:${spec.target} already exists`, + `Location ${input.type}:${input.target} already exists`, ); } const inner: DbLocationsRow = { id: uuid(), - type: spec.type, - target: spec.target, + type: input.type, + target: input.target, }; await tx('locations').insert(inner); diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts similarity index 94% rename from plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts index c2cce66bc6..a3eb5e4542 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts @@ -19,12 +19,16 @@ import { CatalogProcessorEntityResult, CatalogProcessorErrorResult, CatalogProcessorResult, -} from './types'; +} from '../../api'; import path from 'path'; -import { defaultEntityDataParser } from './util/parse'; +import { defaultEntityDataParser } from '../util/parse'; describe('FileReaderProcessor', () => { - const fixturesRoot = path.join(__dirname, '__fixtures__/fileReaderProcessor'); + const fixturesRoot = path.join( + __dirname, + '__fixtures__', + 'fileReaderProcessor', + ); it('should load from file', async () => { const processor = new FileReaderProcessor(); diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts similarity index 87% rename from plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts rename to plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts index 9ff3f52ba0..66c6479fa7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts @@ -14,21 +14,26 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; import fs from 'fs-extra'; import g from 'glob'; import path from 'path'; import { promisify } from 'util'; -import * as result from './results'; import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, -} from './types'; + LocationSpec, + processingResult, +} from '../../api'; const glob = promisify(g); +/** @public */ export class FileReaderProcessor implements CatalogProcessor { + getProcessorName(): string { + return 'FileReaderProcessor'; + } + async readLocation( location: LocationSpec, optional: boolean, @@ -60,11 +65,11 @@ export class FileReaderProcessor implements CatalogProcessor { } } else if (!optional) { const message = `${location.type} ${location.target} does not exist`; - emit(result.notFoundError(location, message)); + emit(processingResult.notFoundError(location, message)); } } catch (e) { const message = `${location.type} ${location.target} could not be read, ${e}`; - emit(result.generalError(location, message)); + emit(processingResult.generalError(location, message)); } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts index c4551e30f8..92ba1fcebf 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations, @@ -22,6 +21,7 @@ import { } from '@backstage/integration'; import path from 'path'; import { toAbsoluteUrl } from './LocationEntityProcessor'; +import { LocationSpec } from '../../api'; describe('LocationEntityProcessor', () => { describe('toAbsoluteUrl', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts similarity index 86% rename from plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts index c6b7def5fc..c168f54bf1 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts @@ -14,11 +14,15 @@ * limitations under the License. */ -import { Entity, LocationEntity, LocationSpec } from '@backstage/catalog-model'; +import { Entity, LocationEntity } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; -import * as result from './results'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; +import { + processingResult, + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, +} from '../../api'; export function toAbsoluteUrl( integrations: ScmIntegrationRegistry, @@ -38,13 +42,19 @@ export function toAbsoluteUrl( } } +/** @public */ export type LocationEntityProcessorOptions = { integrations: ScmIntegrationRegistry; }; +/** @public */ export class LocationEntityProcessor implements CatalogProcessor { constructor(private readonly options: LocationEntityProcessorOptions) {} + getProcessorName(): string { + return 'LocationEntityProcessor'; + } + async postProcessEntity( entity: Entity, location: LocationSpec, @@ -56,7 +66,7 @@ export class LocationEntityProcessor implements CatalogProcessor { const type = locationEntity.spec.type || location.type; if (type === 'file' && location.target.endsWith(path.sep)) { emit( - result.inputError( + processingResult.inputError( location, `LocationEntityProcessor cannot handle ${type} type location with target ${location.target} that ends with a path separator`, ), @@ -77,7 +87,7 @@ export class LocationEntityProcessor implements CatalogProcessor { location, maybeRelativeTarget, ); - emit(result.location({ type, target }, false)); + emit(processingResult.location({ type, target })); } } diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts index c7a49353ae..7bfc66276e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts @@ -154,7 +154,7 @@ describe('PlaceholderProcessor', () => { spec: { data: { $text: '../file.txt' } }, }, { - type: 'github', + type: 'url', target: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', }, @@ -190,7 +190,7 @@ describe('PlaceholderProcessor', () => { spec: { data: { $json: './file.json' } }, }, { - type: 'github', + type: 'url', target: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', }, @@ -224,7 +224,7 @@ describe('PlaceholderProcessor', () => { spec: { data: { $yaml: '../file.yaml' } }, }, { - type: 'github', + type: 'url', target: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', }, @@ -262,7 +262,7 @@ describe('PlaceholderProcessor', () => { }, }, { - type: 'github', + type: 'url', target: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml', }, @@ -300,7 +300,7 @@ describe('PlaceholderProcessor', () => { }, }, { - type: 'github', + type: 'url', target: './a/b/catalog-info.yaml', }, ), @@ -340,7 +340,7 @@ describe('PlaceholderProcessor', () => { }, }, { - type: 'github', + type: 'url', target: './a/b/catalog-info.yaml', }, ), diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts similarity index 96% rename from plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts rename to plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts index 65f65a0479..ddd5f951db 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts @@ -15,18 +15,22 @@ */ import { UrlReader } from '@backstage/backend-common'; -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import yaml from 'yaml'; -import { CatalogProcessor } from './types'; +import { CatalogProcessor, LocationSpec } from '../../api'; +/** @public */ export type PlaceholderResolverRead = (url: string) => Promise; + +/** @public */ export type PlaceholderResolverResolveUrl = ( url: string, base: string, ) => string; +/** @public */ export type PlaceholderResolverParams = { key: string; value: JsonValue; @@ -35,10 +39,12 @@ export type PlaceholderResolverParams = { resolveUrl: PlaceholderResolverResolveUrl; }; +/** @public */ export type PlaceholderResolver = ( params: PlaceholderResolverParams, ) => Promise; +/** @public */ export type PlaceholderProcessorOptions = { resolvers: Record; reader: UrlReader; @@ -48,10 +54,15 @@ export type PlaceholderProcessorOptions = { /** * Traverses raw entity JSON looking for occurrences of $-prefixed placeholders * that it then fills in with actual data. + * @public */ export class PlaceholderProcessor implements CatalogProcessor { constructor(private readonly options: PlaceholderProcessorOptions) {} + getProcessorName(): string { + return 'PlaceholderProcessor'; + } + async preProcessEntity( entity: Entity, location: LocationSpec, diff --git a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts b/plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts similarity index 86% rename from plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts rename to plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts index 425ad6e0fe..e22d011695 100644 --- a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts @@ -14,11 +14,17 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import * as result from './results'; -import { CatalogProcessorEmit } from './types'; +import { + processingResult, + CatalogProcessorEmit, + LocationSpec, +} from '../../api'; +/** + * @deprecated no longer in use, replaced by the ConfigLocationEntityProvider. + * @public + */ export class StaticLocationProcessor implements StaticLocationProcessor { static fromConfig(config: Config): StaticLocationProcessor { const locations: LocationSpec[] = []; @@ -45,7 +51,7 @@ export class StaticLocationProcessor implements StaticLocationProcessor { } for (const staticLocation of this.staticLocations) { - emit(result.location(staticLocation, false)); + emit(processingResult.location(staticLocation)); } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index b7dc0084f2..a96d392c52 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -28,9 +28,9 @@ import { CatalogProcessorEntityResult, CatalogProcessorErrorResult, CatalogProcessorResult, -} from './types'; +} from '../../api'; +import { defaultEntityDataParser } from '../util/parse'; import { UrlReaderProcessor } from './UrlReaderProcessor'; -import { defaultEntityDataParser } from './util/parse'; describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost'; diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts similarity index 91% rename from plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts rename to plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts index 6a85cc07c7..7f27bcd19d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts @@ -15,12 +15,11 @@ */ import { UrlReader } from '@backstage/backend-common'; -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { assertError } from '@backstage/errors'; import parseGitUrl from 'git-url-parse'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; -import * as result from './results'; import { CatalogProcessor, CatalogProcessorCache, @@ -28,15 +27,12 @@ import { CatalogProcessorEntityResult, CatalogProcessorParser, CatalogProcessorResult, -} from './types'; + LocationSpec, + processingResult, +} from '../../api'; const CACHE_KEY = 'v1'; -type Options = { - reader: UrlReader; - logger: Logger; -}; - // WARNING: If you change this type, you likely need to bump the CACHE_KEY as well type CacheItem = { etag: string; @@ -47,8 +43,14 @@ type CacheItem = { }[]; }; +/** @public */ export class UrlReaderProcessor implements CatalogProcessor { - constructor(private readonly options: Options) {} + constructor( + private readonly options: { + reader: UrlReader; + logger: Logger; + }, + ) {} getProcessorName() { return 'url-reader'; @@ -100,10 +102,10 @@ export class UrlReaderProcessor implements CatalogProcessor { } } else if (error.name === 'NotFoundError') { if (!optional) { - emit(result.notFoundError(location, message)); + emit(processingResult.notFoundError(location, message)); } } else { - emit(result.generalError(location, message)); + emit(processingResult.generalError(location, message)); } } diff --git a/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt new file mode 100644 index 0000000000..7470c0e8a3 --- /dev/null +++ b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt @@ -0,0 +1 @@ +site_name: Test diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml rename to plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml rename to plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/test.txt similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt rename to plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/test.txt diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/modules/core/index.ts similarity index 60% rename from plugins/catalog-backend/src/ingestion/processors/index.ts rename to plugins/catalog-backend/src/modules/core/index.ts index 63feac51af..9d582bfb34 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/modules/core/index.ts @@ -14,22 +14,10 @@ * limitations under the License. */ -import * as results from './results'; - export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; export { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; -export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor'; -export type { AwsOrganizationProviderConfig } from './awsOrganization/config'; -export { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; -export { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; -export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; -export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; -export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; -export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; -export { GithubMultiOrgReaderProcessor } from './GithubMultiOrgReaderProcessor'; -export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; export { LocationEntityProcessor } from './LocationEntityProcessor'; export type { LocationEntityProcessorOptions } from './LocationEntityProcessor'; export { PlaceholderProcessor } from './PlaceholderProcessor'; @@ -41,9 +29,5 @@ export type { PlaceholderResolverResolveUrl, } from './PlaceholderProcessor'; export { StaticLocationProcessor } from './StaticLocationProcessor'; -export * from './types'; export { UrlReaderProcessor } from './UrlReaderProcessor'; -export { parseEntityYaml } from './util/parse'; -export { results }; - -export type { BitbucketRepositoryParser } from './bitbucket'; +export { parseEntityYaml } from '../util/parse'; diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.test.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts rename to plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.test.ts index e7aec1520d..dd3bf490fb 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.test.ts @@ -20,10 +20,12 @@ import { GithubCredentialsProvider, GitHubIntegrationConfig, } from '@backstage/integration'; -import { GitHubOrgEntityProvider } from '.'; -import { EntityProviderConnection } from '../../providers'; -import { withLocations } from './GitHubOrgEntityProvider'; import { graphql } from '@octokit/graphql'; +import { EntityProviderConnection } from '../../api'; +import { + GitHubOrgEntityProvider, + withLocations, +} from './GitHubOrgEntityProvider'; jest.mock('@octokit/graphql'); diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts rename to plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.ts index b1d44a55b5..76389968ae 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts +++ b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.ts @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * 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 { @@ -29,16 +30,16 @@ import { import { graphql } from '@octokit/graphql'; import { merge } from 'lodash'; import { Logger } from 'winston'; -import { EntityProvider, EntityProviderConnection } from '../../providers'; +import { EntityProvider, EntityProviderConnection } from '../../api'; import { getOrganizationTeams, getOrganizationUsers, parseGitHubOrgUrl, -} from '../processors/github'; -import { assignGroupsToUsers, buildOrgHierarchy } from '../processors/util/org'; +} from './lib'; +import { assignGroupsToUsers, buildOrgHierarchy } from '../util/org'; // TODO: Consider supporting an (optional) webhook that reacts on org changes - +/** @public */ export class GitHubOrgEntityProvider implements EntityProvider { private connection?: EntityProviderConnection; private githubCredentialsProvider: GithubCredentialsProvider; @@ -178,8 +179,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/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.test.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.test.ts index d6746f13da..5440539b27 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.test.ts @@ -15,16 +15,16 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { LocationSpec } from '@backstage/catalog-model'; -import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; -import { getOrganizationRepositories } from './github'; import { ConfigReader } from '@backstage/config'; import { - ScmIntegrations, DefaultGithubCredentialsProvider, + ScmIntegrations, } from '@backstage/integration'; +import { LocationSpec } from '../../api'; +import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; +import { getOrganizationRepositories } from './lib'; -jest.mock('./github'); +jest.mock('./lib'); const mockGetOrganizationRepositories = getOrganizationRepositories as jest.MockedFunction< typeof getOrganizationRepositories @@ -175,7 +175,6 @@ describe('GithubDiscoveryProcessor', () => { 'https://github.com/backstage/backstage/blob/master/catalog.yaml', presence: 'optional', }, - optional: true, }); expect(emitter).toHaveBeenCalledWith({ type: 'location', @@ -184,7 +183,6 @@ describe('GithubDiscoveryProcessor', () => { target: 'https://github.com/backstage/demo/blob/master/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -217,7 +215,6 @@ describe('GithubDiscoveryProcessor', () => { 'https://github.com/backstage/tech-docs/blob/main/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -272,7 +269,6 @@ describe('GithubDiscoveryProcessor', () => { 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', presence: 'optional', }, - optional: true, }); }); @@ -328,7 +324,6 @@ describe('GithubDiscoveryProcessor', () => { 'https://github.com/backstage/techdocs-cli/blob/master/catalog.yaml', presence: 'optional', }, - optional: true, }); expect(emitter).toHaveBeenCalledWith({ type: 'location', @@ -338,7 +333,6 @@ describe('GithubDiscoveryProcessor', () => { 'https://github.com/backstage/techdocs-container/blob/master/catalog.yaml', presence: 'optional', }, - optional: true, }); }); @@ -394,7 +388,6 @@ describe('GithubDiscoveryProcessor', () => { target: 'https://github.com/backstage/test/blob/master/catalog.yaml', presence: 'optional', }, - optional: true, }); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.ts similarity index 91% rename from plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.ts index 3196c4e161..4aff470f52 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.ts @@ -14,18 +14,22 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, + ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { Logger } from 'winston'; -import { getOrganizationRepositories } from './github'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; +import { getOrganizationRepositories } from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; /** * Extracts repositories out of a GitHub org. @@ -40,9 +44,11 @@ import { CatalogProcessor, CatalogProcessorEmit } from './types'; * You may also explicitly specify the source branch: * * target: https://github.com/backstage/*\/blob/main/catalog-info.yaml + * + * @public **/ export class GithubDiscoveryProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; private readonly githubCredentialsProvider: GithubCredentialsProvider; @@ -62,7 +68,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { } constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }) { @@ -72,6 +78,9 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { options.githubCredentialsProvider || DefaultGithubCredentialsProvider.fromIntegrations(this.integrations); } + getProcessorName(): string { + return 'GithubDiscoveryProcessor'; + } async readLocation( location: LocationSpec, @@ -136,17 +145,14 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { const path = `/blob/${branchName}${catalogPath}`; emit( - results.location( - { - type: 'url', - target: `${repository.url}${path}`, - presence: 'optional', - }, + processingResult.location({ + type: 'url', + target: `${repository.url}${path}`, // Not all locations may actually exist, since the user defined them as a wildcard pattern. // Thus, we emit them as optional and let the downstream processor find them while not outputting // an error if it couldn't. - true, - ), + presence: 'optional', + }), ); } diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts rename to plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts index dd705ad691..339119137a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubAppCredentialsMux, GithubCredentialsProvider, GitHubIntegrationConfig, + ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; @@ -30,10 +30,14 @@ import { getOrganizationUsers, GithubMultiOrgConfig, readGithubMultiOrgConfig, -} from './github'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; -import { buildOrgHierarchy } from './util/org'; +} from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { buildOrgHierarchy } from '../util/org'; /** * @alpha @@ -42,7 +46,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 +69,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { } constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger; orgs: GithubMultiOrgConfig; githubCredentialsProvider?: GithubCredentialsProvider; @@ -77,6 +81,9 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { options.githubCredentialsProvider || DefaultGithubCredentialsProvider.fromIntegrations(this.integrations); } + getProcessorName(): string { + return 'GithubMultiOrgReaderProcessor'; + } async readLocation( location: LocationSpec, @@ -155,7 +162,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { buildOrgHierarchy(groups); for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } } catch (e) { this.logger.error( @@ -166,7 +173,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { const allUsers = Array.from(allUsersMap.values()); for (const user of allUsers) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts rename to plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.test.ts index 953c926c71..9ce278ac44 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.test.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger } from '@backstage/backend-common'; -import { LocationSpec } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations, @@ -22,6 +22,7 @@ import { } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; +import { LocationSpec } from '../../api'; jest.mock('@octokit/graphql'); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.ts similarity index 87% rename from plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts rename to plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.ts index 474d08feee..e85e7b874d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { GithubCredentialType, + ScmIntegrationRegistry, ScmIntegrations, GithubCredentialsProvider, DefaultGithubCredentialsProvider, @@ -28,18 +28,23 @@ import { getOrganizationTeams, getOrganizationUsers, parseGitHubOrgUrl, -} from './github'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; -import { assignGroupsToUsers, buildOrgHierarchy } from './util/org'; +} from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { assignGroupsToUsers, buildOrgHierarchy } from '../util/org'; type GraphQL = typeof graphql; /** * Extracts teams and users out of a GitHub org. + * @public */ export class GithubOrgReaderProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; private readonly githubCredentialsProvider: GithubCredentialsProvider; @@ -59,7 +64,7 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { } constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }) { @@ -69,6 +74,9 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { DefaultGithubCredentialsProvider.fromIntegrations(this.integrations); this.logger = options.logger; } + getProcessorName(): string { + return 'GithubOrgReaderProcessor'; + } async readLocation( location: LocationSpec, @@ -102,10 +110,10 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { // Done! for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } for (const user of users) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend/src/modules/github/index.ts b/plugins/catalog-backend/src/modules/github/index.ts new file mode 100644 index 0000000000..7958818096 --- /dev/null +++ b/plugins/catalog-backend/src/modules/github/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; +export { GithubMultiOrgReaderProcessor } from './GithubMultiOrgReaderProcessor'; +export { GitHubOrgEntityProvider } from './GitHubOrgEntityProvider'; +export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; +export type { GithubMultiOrgConfig } from './lib'; diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts b/plugins/catalog-backend/src/modules/github/lib/config.test.ts similarity index 55% rename from plugins/catalog-backend/src/ingestion/processors/github/config.test.ts rename to plugins/catalog-backend/src/modules/github/lib/config.test.ts index a67d2378d0..3b722a0e7b 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts +++ b/plugins/catalog-backend/src/modules/github/lib/config.test.ts @@ -15,68 +15,9 @@ */ import { ConfigReader } from '@backstage/config'; -import { readGithubConfig, readGithubMultiOrgConfig } from './config'; +import { readGithubMultiOrgConfig } from './config'; describe('config', () => { - describe('readGithubConfig', () => { - function config( - providers: { target: string; apiBaseUrl?: string; token?: string }[], - ) { - return new ConfigReader({ - catalog: { processors: { githubOrg: { providers } } }, - }); - } - - it('adds a default GitHub entry when missing', () => { - const output = readGithubConfig(config([])); - expect(output).toEqual([ - { - target: 'https://github.com', - apiBaseUrl: 'https://api.github.com', - }, - ]); - }); - - it('injects the correct GitHub API base URL when missing', () => { - const output = readGithubConfig( - config([{ target: 'https://github.com' }]), - ); - expect(output).toEqual([ - { - target: 'https://github.com', - apiBaseUrl: 'https://api.github.com', - }, - ]); - }); - - it('rejects custom targets with no base URLs', () => { - expect(() => - readGithubConfig(config([{ target: 'https://ghe.company.com' }])), - ).toThrow( - 'Provider at https://ghe.company.com must configure an explicit apiBaseUrl', - ); - }); - - it('rejects funky configs', () => { - expect(() => readGithubConfig(config([{ target: 7 } as any]))).toThrow( - /target/, - ); - expect(() => - readGithubConfig(config([{ noTarget: '7' } as any])), - ).toThrow(/target/); - expect(() => - readGithubConfig( - config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]), - ), - ).toThrow(/apiBaseUrl/); - expect(() => - readGithubConfig( - config([{ target: 'https://github.com', token: 7 } as any]), - ), - ).toThrow(/token/); - }); - }); - describe('readGithubMultiOrgConfig', () => { function config( orgs: { name: string; groupNamespace?: string; userNamespace?: string }[], diff --git a/plugins/catalog-backend/src/modules/github/lib/config.ts b/plugins/catalog-backend/src/modules/github/lib/config.ts new file mode 100644 index 0000000000..242296b4be --- /dev/null +++ b/plugins/catalog-backend/src/modules/github/lib/config.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; + +/** + * The configuration parameters for a multi-org GitHub processor. + * @public + */ +export type GithubMultiOrgConfig = Array<{ + /** + * The name of the GitHub org to process. + */ + name: string; + /** + * The namespace of the group created for this org. + */ + groupNamespace: string; + /** + * The namespace of the users created for this org. If not specified defaults to undefined. + */ + userNamespace: string | undefined; +}>; + +export function readGithubMultiOrgConfig(config: Config): GithubMultiOrgConfig { + const orgConfigs = config.getOptionalConfigArray('orgs') ?? []; + return orgConfigs.map(c => ({ + name: c.getString('name'), + groupNamespace: ( + c.getOptionalString('groupNamespace') ?? c.getString('name') + ).toLowerCase(), + userNamespace: c.getOptionalString('userNamespace') ?? undefined, + })); +} diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts b/plugins/catalog-backend/src/modules/github/lib/github.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/github.test.ts rename to plugins/catalog-backend/src/modules/github/lib/github.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.ts b/plugins/catalog-backend/src/modules/github/lib/github.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/github.ts rename to plugins/catalog-backend/src/modules/github/lib/github.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/index.ts b/plugins/catalog-backend/src/modules/github/lib/index.ts similarity index 84% rename from plugins/catalog-backend/src/ingestion/processors/github/index.ts rename to plugins/catalog-backend/src/modules/github/lib/index.ts index 3d9e881ddd..3e31d9e00c 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/index.ts +++ b/plugins/catalog-backend/src/modules/github/lib/index.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -export { readGithubConfig, readGithubMultiOrgConfig } from './config'; -export type { GithubMultiOrgConfig, ProviderConfig } from './config'; +export { readGithubMultiOrgConfig } from './config'; +export type { GithubMultiOrgConfig } from './config'; export { getOrganizationRepositories, getOrganizationTeams, diff --git a/plugins/catalog-backend/src/ingestion/processors/github/util.test.ts b/plugins/catalog-backend/src/modules/github/lib/util.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/github/util.test.ts rename to plugins/catalog-backend/src/modules/github/lib/util.test.ts index 02f54e0675..c73c11bd2d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/util.test.ts +++ b/plugins/catalog-backend/src/modules/github/lib/util.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { parseGitHubOrgUrl } from './util'; describe('parseGitHubOrgUrl', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/github/util.ts b/plugins/catalog-backend/src/modules/github/lib/util.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/github/util.ts rename to plugins/catalog-backend/src/modules/github/lib/util.ts index d8df376038..a38225ff98 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/util.ts +++ b/plugins/catalog-backend/src/modules/github/lib/util.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export function parseGitHubOrgUrl(urlString: string): { org: string } { const path = new URL(urlString).pathname.substr(1).split('/'); diff --git a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.test.ts index 8d85d7375f..cc0f368aee 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.test.ts @@ -16,11 +16,11 @@ import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; -import { LocationSpec } from '@backstage/catalog-model'; import { GitLabDiscoveryProcessor, parseUrl } from './GitLabDiscoveryProcessor'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; -import { GitLabProject } from './gitlab'; +import { GitLabProject } from './lib'; +import { LocationSpec } from '../../api'; const server = setupServer(); @@ -210,7 +210,6 @@ describe('GitlabDiscoveryProcessor', () => { target: 'https://gitlab.fake/1/-/blob/main/catalog-info.yaml', presence: 'optional', }, - optional: true, }, { type: 'location', @@ -219,7 +218,6 @@ describe('GitlabDiscoveryProcessor', () => { target: 'https://gitlab.fake/2/-/blob/master/catalog-info.yaml', presence: 'optional', }, - optional: true, }, ]); }); @@ -257,7 +255,6 @@ describe('GitlabDiscoveryProcessor', () => { target: 'https://gitlab.fake/1/-/blob/master/catalog-info.yaml', presence: 'optional', }, - optional: true, }, ]); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.ts similarity index 71% rename from plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.ts index fabf3a1136..d3916f45d1 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.ts @@ -14,13 +14,19 @@ * limitations under the License. */ -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'; -import { GitLabClient, GitLabProject, paginated } from './gitlab'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { GitLabClient, GitLabProject, paginated } from './lib'; import { CacheClient, CacheManager, @@ -29,9 +35,10 @@ import { /** * Extracts repositories out of an GitLab instance. + * @public */ export class GitLabDiscoveryProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; private readonly cache: CacheClient; @@ -48,7 +55,7 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { } private constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; pluginCache: PluginCacheManager; logger: Logger; }) { @@ -57,6 +64,10 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { this.logger = options.logger; } + getProcessorName(): string { + return 'GitLabDiscoveryProcessor'; + } + async readLocation( location: LocationSpec, _optional: boolean, @@ -88,12 +99,12 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { page: 1, }); - const result: Result = { + const res: Result = { scanned: 0, matches: [], }; for await (const project of projects) { - result.scanned++; + res.scanned++; if (project.archived) { continue; @@ -103,41 +114,39 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { continue; } - result.matches.push(project); + res.matches.push(project); } - for (const project of result.matches) { + for (const project of res.matches) { const project_branch = branch === '*' ? project.default_branch : branch; emit( - results.location( - { - type: 'url', - // The format expected by the GitLabUrlReader: - // https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath - // - // This unfortunately will trigger another API call in `getGitLabFileFetchUrl` to get the project ID. - // The alternative is using the `buildRawUrl` function, which does not support subgroups, so providing a raw - // URL here won't work either. - target: `${project.web_url}/-/blob/${project_branch}/${catalogPath}`, - presence: 'optional', - }, - true, - ), + processingResult.location({ + type: 'url', + // The format expected by the GitLabUrlReader: + // https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath + // + // This unfortunately will trigger another API call in `getGitLabFileFetchUrl` to get the project ID. + // The alternative is using the `buildRawUrl` function, which does not support subgroups, so providing a raw + // URL here won't work either. + target: `${project.web_url}/-/blob/${project_branch}/${catalogPath}`, + presence: 'optional', + }), ); } const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); this.logger.debug( - `Read ${result.scanned} GitLab repositories in ${duration} seconds`, + `Read ${res.scanned} GitLab repositories in ${duration} seconds`, ); return true; } - async updateLastActivity(): Promise { - const lastActivity = await this.cache.get('last-activity'); - await this.cache.set('last-activity', new Date().toISOString()); + private async updateLastActivity(): Promise { + const cacheKey = `processors/${this.getProcessorName()}/last-activity`; + const lastActivity = await this.cache.get(cacheKey); + await this.cache.set(cacheKey, new Date().toISOString()); return lastActivity as string | undefined; } } diff --git a/plugins/catalog-backend/src/modules/gitlab/index.ts b/plugins/catalog-backend/src/modules/gitlab/index.ts new file mode 100644 index 0000000000..f42a901f8f --- /dev/null +++ b/plugins/catalog-backend/src/modules/gitlab/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 { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts b/plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts rename to plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts index b249137740..a7c0d5161f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts +++ b/plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ConfigReader } from '@backstage/config'; import { setupRequestMockHandlers } from '@backstage/test-utils'; import { readGitLabIntegrationConfig } from '@backstage/integration'; import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer, SetupServerApi } from 'msw/node'; - import { GitLabClient, paginated } from './client'; const server = setupServer(); diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts b/plugins/catalog-backend/src/modules/gitlab/lib/client.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts rename to plugins/catalog-backend/src/modules/gitlab/lib/client.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/index.ts b/plugins/catalog-backend/src/modules/gitlab/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/index.ts rename to plugins/catalog-backend/src/modules/gitlab/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/types.ts b/plugins/catalog-backend/src/modules/gitlab/lib/types.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/types.ts rename to plugins/catalog-backend/src/modules/gitlab/lib/types.ts diff --git a/plugins/catalog-backend/src/modules/index.ts b/plugins/catalog-backend/src/modules/index.ts new file mode 100644 index 0000000000..6acf057ca3 --- /dev/null +++ b/plugins/catalog-backend/src/modules/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './aws'; +export * from './azure'; +export * from './bitbucket'; +export * from './codeowners'; +export * from './core'; +export * from './github'; +export * from './gitlab'; diff --git a/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts b/plugins/catalog-backend/src/modules/util/org.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/util/org.test.ts rename to plugins/catalog-backend/src/modules/util/org.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/util/org.ts b/plugins/catalog-backend/src/modules/util/org.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/util/org.ts rename to plugins/catalog-backend/src/modules/util/org.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts b/plugins/catalog-backend/src/modules/util/parse.test.ts similarity index 91% rename from plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts rename to plugins/catalog-backend/src/modules/util/parse.test.ts index a42d160725..e87de1c9a3 100644 --- a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts +++ b/plugins/catalog-backend/src/modules/util/parse.test.ts @@ -15,7 +15,7 @@ */ import { parseEntityYaml } from './parse'; -import * as result from '../results'; +import { processingResult } from '../../api'; const testLoc = { target: 'my-loc-target', @@ -47,7 +47,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -92,7 +92,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -102,7 +102,7 @@ describe('parseEntityYaml', () => { type: 'website', }, }), - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -137,7 +137,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -157,7 +157,7 @@ describe('parseEntityYaml', () => { // Parse errors are always per document expect(results).toEqual([ - result.generalError( + processingResult.generalError( testLoc, 'YAML error at my-loc-type:my-loc-target, YAMLSemanticError: Plain value cannot start with reserved character `', ), @@ -186,7 +186,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -196,7 +196,7 @@ describe('parseEntityYaml', () => { type: 'website', }, }), - result.generalError( + processingResult.generalError( testLoc, 'YAML error at my-loc-type:my-loc-target, YAMLSemanticError: Nested mappings are not allowed in compact mappings', ), @@ -209,7 +209,10 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.generalError(testLoc, 'Expected object at root, got string'), + processingResult.generalError( + testLoc, + 'Expected object at root, got string', + ), ]); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts b/plugins/catalog-backend/src/modules/util/parse.ts similarity index 76% rename from plugins/catalog-backend/src/ingestion/processors/util/parse.ts rename to plugins/catalog-backend/src/modules/util/parse.ts index c9c6c79311..65fad5986f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts +++ b/plugins/catalog-backend/src/modules/util/parse.ts @@ -14,16 +14,17 @@ * limitations under the License. */ -import { - Entity, - LocationSpec, - stringifyLocationReference, -} from '@backstage/catalog-model'; +import { Entity, stringifyLocationRef } from '@backstage/catalog-model'; import lodash from 'lodash'; import yaml from 'yaml'; -import * as result from '../results'; -import { CatalogProcessorParser, CatalogProcessorResult } from '../types'; +import { + CatalogProcessorParser, + CatalogProcessorResult, + LocationSpec, + processingResult, +} from '../../api'; +/** @public */ export function* parseEntityYaml( data: Buffer, location: LocationSpec, @@ -32,27 +33,27 @@ 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); + yield processingResult.generalError(location, message); return; } 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); + yield processingResult.generalError(location, message); } else { const json = document.toJSON(); if (lodash.isPlainObject(json)) { - yield result.entity(location, json as Entity); + yield processingResult.entity(location, json as Entity); } else if (json === null) { // Ignore null values, these happen if there is an empty document in the // YAML file, for example if --- is added to the end of the file. } else { const message = `Expected object at root, got ${typeof json}`; - yield result.generalError(location, message); + yield processingResult.generalError(location, message); } } } diff --git a/plugins/catalog-backend/src/permissions/conditionExports.ts b/plugins/catalog-backend/src/permissions/conditionExports.ts index 28e74c53c2..2846c8c3ec 100644 --- a/plugins/catalog-backend/src/permissions/conditionExports.ts +++ b/plugins/catalog-backend/src/permissions/conditionExports.ts @@ -27,7 +27,8 @@ const conditionExports = createConditionExports({ /** * These conditions are used when creating conditional decisions that are returned * by authorization policies. - * @public + * + * @alpha */ export const catalogConditions = conditionExports.conditions; @@ -50,7 +51,8 @@ export const catalogConditions = conditionExports.conditions; * } * } * ``` - * @public + * + * @alpha */ export const createCatalogPolicyDecision = conditionExports.createPolicyDecision; diff --git a/plugins/catalog-backend/src/permissions/rules/index.ts b/plugins/catalog-backend/src/permissions/rules/index.ts index 4eec796c74..f9d192781e 100644 --- a/plugins/catalog-backend/src/permissions/rules/index.ts +++ b/plugins/catalog-backend/src/permissions/rules/index.ts @@ -24,7 +24,8 @@ import { hasSpec } from './hasSpec'; /** * These permission rules can be used to conditionally filter catalog entities * or describe a user's access to the entities. - * @public + * + * @alpha */ export const permissionRules = { hasAnnotation, diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.test.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.test.ts index 282d2558ef..8e20655393 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.test.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.test.ts @@ -29,6 +29,7 @@ describe('isEntityOwner', () => { relations: [ { type: 'ownedBy', + targetRef: 'user:default/spiderman', target: { kind: 'user', namespace: 'default', @@ -52,6 +53,7 @@ describe('isEntityOwner', () => { relations: [ { type: 'ownedBy', + targetRef: 'user:default/green-goblin', target: { kind: 'user', namespace: 'default', diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts index 3176b31b87..a6dda13c29 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -14,16 +14,13 @@ * limitations under the License. */ -import { - Entity, - RELATION_OWNED_BY, - stringifyEntityRef, -} from '@backstage/catalog-model'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { createCatalogPermissionRule } from './util'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for entities with a specified owner. + * * @public */ export const isEntityOwner = createCatalogPermissionRule({ @@ -36,7 +33,7 @@ export const isEntityOwner = createCatalogPermissionRule({ return resource.relations .filter(relation => relation.type === RELATION_OWNED_BY) - .some(relation => claims.includes(stringifyEntityRef(relation.target))); + .some(relation => claims.includes(relation.targetRef)); }, toQuery: (claims: string[]) => ({ key: 'relations.ownedBy', diff --git a/plugins/catalog-backend/src/permissions/rules/util.ts b/plugins/catalog-backend/src/permissions/rules/util.ts index 7ef0ca3546..a1316c7fb2 100644 --- a/plugins/catalog-backend/src/permissions/rules/util.ts +++ b/plugins/catalog-backend/src/permissions/rules/util.ts @@ -23,7 +23,7 @@ import { EntitiesSearchFilter } from '../../catalog/types'; * {@link @backstage/plugin-permission-node#PermissionRule}s for the * catalog-backend. * - * @public + * @alpha */ export const createCatalogPermissionRule = makeCreatePermissionRule< Entity, diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts index 5ae4d58ec7..6e494d784e 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts @@ -16,12 +16,11 @@ 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 { @@ -29,11 +28,12 @@ import { CatalogProcessorCache, CatalogProcessorEmit, CatalogProcessorParser, - results, -} from '../ingestion'; + LocationSpec, + processingResult, +} from '../api'; import { CatalogRulesEnforcer } from '../ingestion/CatalogRules'; import { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator'; -import { defaultEntityDataParser } from '../ingestion/processors/util/parse'; +import { defaultEntityDataParser } from '../modules/util/parse'; import { ConfigReader } from '@backstage/config'; class FooBarProcessor implements CatalogProcessor { @@ -51,7 +51,7 @@ class FooBarProcessor implements CatalogProcessor { ) { if (await cache.get('emit')) { emit( - results.entity( + processingResult.entity( { type: 'url', target: './new-place' }, { apiVersion: 'my-api/v1', @@ -63,7 +63,7 @@ class FooBarProcessor implements CatalogProcessor { ), ); emit( - results.relation({ + processingResult.relation({ type: 'my-type', source: { kind: 'foobar', name: 'my-source', namespace: 'default' }, target: { kind: 'foobar', name: 'my-target', namespace: 'default' }, @@ -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: { @@ -208,9 +208,10 @@ describe('DefaultCatalogProcessingOrchestrator', () => { const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); const processor: jest.Mocked = { + getProcessorName: jest.fn(), validateEntityKind: jest.fn(async () => true), readLocation: jest.fn(async (_l, _o, emit) => { - emit(results.entity({ type: 't', target: 't' }, entity)); + emit(processingResult.entity({ type: 't', target: 't' }, entity)); return true; }), }; diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 756b6a3639..c050779a8b 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -18,10 +18,9 @@ import { Entity, EntityPolicy, LocationEntity, - LocationSpec, - parseLocationReference, + parseLocationRef, stringifyEntityRef, - stringifyLocationReference, + stringifyLocationRef, } from '@backstage/catalog-model'; import { assertError, @@ -36,8 +35,9 @@ import { Logger } from 'winston'; import { CatalogProcessor, CatalogProcessorParser, -} from '../ingestion/processors'; -import * as results from '../ingestion/processors/results'; + LocationSpec, + processingResult, +} from '../api'; import { CatalogProcessingOrchestrator, EntityProcessingRequest, @@ -64,6 +64,7 @@ type Context = { cache: ProcessorCacheManager; }; +/** @public */ export class DefaultCatalogProcessingOrchestrator implements CatalogProcessingOrchestrator { @@ -119,10 +120,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 +148,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`, ); @@ -179,13 +178,13 @@ export class DefaultCatalogProcessingOrchestrator entity: Entity, context: Context, ): Promise { - let result = entity; + let res = entity; for (const processor of this.options.processors) { if (processor.preProcessEntity) { try { - result = await processor.preProcessEntity( - result, + res = await processor.preProcessEntity( + res, context.location, context.collector.onEmit, context.originLocation, @@ -200,7 +199,7 @@ export class DefaultCatalogProcessingOrchestrator } } - return result; + return res; } /** @@ -296,7 +295,7 @@ export class DefaultCatalogProcessingOrchestrator for (const maybeRelativeTarget of targets) { if (type === 'file' && maybeRelativeTarget.endsWith(path.sep)) { context.collector.onEmit( - results.inputError( + processingResult.inputError( context.location, `LocationEntityProcessor cannot handle ${type} type location with target ${context.location.target} that ends with a path separator`, ), @@ -352,13 +351,13 @@ export class DefaultCatalogProcessingOrchestrator entity: Entity, context: Context, ): Promise { - let result = entity; + let res = entity; for (const processor of this.options.processors) { if (processor.postProcessEntity) { try { - result = await processor.postProcessEntity( - result, + res = await processor.postProcessEntity( + res, context.location, context.collector.onEmit, context.cache.forProcessor(processor), @@ -372,6 +371,6 @@ export class DefaultCatalogProcessingOrchestrator } } - return result; + return res; } } diff --git a/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts b/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts index e7f9fb313b..602557a224 100644 --- a/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts +++ b/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts @@ -14,14 +14,18 @@ * limitations under the License. */ -import { CatalogProcessor } from '../ingestion/processors'; +import { CatalogProcessor } from '../api'; import { ProcessorCacheManager } from './ProcessorCacheManager'; class MyProcessor implements CatalogProcessor { getProcessorName = () => 'my-processor'; } -class OtherProcessor implements CatalogProcessor {} +class OtherProcessor implements CatalogProcessor { + getProcessorName(): string { + return 'OtherProcessor'; + } +} describe('ProcessorCacheManager', () => { const myProcessor = new MyProcessor(); diff --git a/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts b/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts index 472d6453ce..e54641f572 100644 --- a/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts +++ b/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts @@ -15,8 +15,7 @@ */ import { JsonObject, JsonValue } from '@backstage/types'; -import { CatalogProcessor } from '../ingestion/processors'; -import { CatalogProcessorCache } from '../ingestion/processors/types'; +import { CatalogProcessor, CatalogProcessorCache } from '../api'; import { isObject } from './util'; class SingleProcessorSubCache implements CatalogProcessorCache { @@ -104,7 +103,7 @@ export class ProcessorCacheManager { key?: string, ): CatalogProcessorCache { // constructor name will be deprecated in the future when we make `getProcessorName` required in the implementation - const name = processor.getProcessorName?.() ?? processor.constructor.name; + const name = processor.getProcessorName(); const cache = this.caches.get(name); if (cache) { return key ? cache.withKey(key) : cache; diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index 4ac356f706..c3cc6e288b 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -16,14 +16,13 @@ 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'; -import { CatalogProcessorResult } from '../ingestion'; +import { CatalogProcessorResult, EntityRelationSpec } from '../api'; import { locationSpecToLocationEntity } from '../util/conversion'; import { DeferredEntity } from './types'; import { @@ -82,7 +81,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 +97,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/connectEntityProviders.ts b/plugins/catalog-backend/src/processing/connectEntityProviders.ts index 02bdc2aee3..d7015ea52e 100644 --- a/plugins/catalog-backend/src/processing/connectEntityProviders.ts +++ b/plugins/catalog-backend/src/processing/connectEntityProviders.ts @@ -23,7 +23,7 @@ import { EntityProvider, EntityProviderConnection, EntityProviderMutation, -} from '../providers/types'; +} from '../api'; class Connection implements EntityProviderConnection { readonly validateEntityEnvelope = entityEnvelopeSchemaValidator(); diff --git a/plugins/catalog-backend/src/processing/refresh.ts b/plugins/catalog-backend/src/processing/refresh.ts index c60f42ff96..3e03e3add8 100644 --- a/plugins/catalog-backend/src/processing/refresh.ts +++ b/plugins/catalog-backend/src/processing/refresh.ts @@ -16,12 +16,14 @@ /** * Function that returns the catalog refresh interval in seconds. + * @public */ export type RefreshIntervalFunction = () => number; /** * Creates a function that returns a random refresh interval between minSeconds and maxSeconds. * @returns A {@link RefreshIntervalFunction} that provides the next refresh interval + * @public */ export function createRandomRefreshInterval(options: { minSeconds: number; diff --git a/plugins/catalog-backend/src/processing/types.ts b/plugins/catalog-backend/src/processing/types.ts index 7fb1157d3b..484cd73939 100644 --- a/plugins/catalog-backend/src/processing/types.ts +++ b/plugins/catalog-backend/src/processing/types.ts @@ -14,14 +14,23 @@ * limitations under the License. */ -import { Entity, EntityRelationSpec } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; +import { EntityRelationSpec } from '../api'; +/** + * The request to process an entity. + * @public + */ export type EntityProcessingRequest = { entity: Entity; state?: JsonObject; // Versions for multiple deployments etc }; +/** + * The result of processing an entity. + * @public + */ export type EntityProcessingResult = | { ok: true; @@ -36,15 +45,24 @@ export type EntityProcessingResult = errors: Error[]; }; +/** + * Responsible for executing the individual processing steps in order to fully process an entity. + * @public + * */ export interface CatalogProcessingOrchestrator { process(request: EntityProcessingRequest): Promise; } +/** + * Entities that are not yet processed. + * @public + * */ export type DeferredEntity = { entity: Entity; locationKey?: string; }; +/** @public */ export interface CatalogProcessingEngine { start(): Promise; stop(): Promise; diff --git a/plugins/catalog-backend/src/processing/util.ts b/plugins/catalog-backend/src/processing/util.ts index 9f5f23ed09..b1948cf559 100644 --- a/plugins/catalog-backend/src/processing/util.ts +++ b/plugins/catalog-backend/src/processing/util.ts @@ -19,35 +19,37 @@ import { entityEnvelopeSchemaValidator, entitySchemaValidator, LocationEntity, - LocationSpec, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, stringifyEntityRef, } from '@backstage/catalog-model'; import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; +import { LocationSpec } from '../api'; export function isLocationEntity(entity: Entity): entity is LocationEntity { return entity.kind === 'Location'; } 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`); + throw new InputError( + `Entity '${entityRef}' does not have the annotation ${ANNOTATION_LOCATION}`, + ); } return ref; } 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( - `Entity '${entityRef}' does not have an origin location`, + `Entity '${entityRef}' does not have the annotation ${ANNOTATION_ORIGIN_LOCATION}`, ); } return ref; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 93ef6968d1..c59318d0af 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -23,27 +23,24 @@ import { stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import { IndexableDocument, DocumentCollator } from '@backstage/search-common'; import { Config } from '@backstage/config'; import { CatalogApi, CatalogClient, - CatalogEntitiesRequest, + GetEntitiesRequest, } from '@backstage/catalog-client'; import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; +import { CatalogEntityDocument } from './DefaultCatalogCollatorFactory'; -export interface CatalogEntityDocument extends IndexableDocument { - componentType: string; - namespace: string; - kind: string; - lifecycle: string; - owner: string; -} - -export class DefaultCatalogCollator implements DocumentCollator { +/** + * @public + * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and + * use `DefaultCatalogCollatorFactory` instead. + */ +export class DefaultCatalogCollator { 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 +51,7 @@ export class DefaultCatalogCollator implements DocumentCollator { options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; - filter?: CatalogEntitiesRequest['filter']; + filter?: GetEntitiesRequest['filter']; }, ) { return new DefaultCatalogCollator({ @@ -66,7 +63,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/search/DefaultCatalogCollatorFactory.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts new file mode 100644 index 0000000000..ccb61b8680 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts @@ -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 { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { Readable } from 'stream'; +import { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; + +const server = setupServer(); + +const expectedEntities: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-entity', + description: 'The expected description', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity', + name: 'test-entity-2', + description: 'The expected description 2', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, + }, +]; + +describe('DefaultCatalogCollatorFactory', () => { + const config = new ConfigReader({}); + const mockDiscoveryApi: jest.Mocked = { + getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'), + getExternalBaseUrl: jest.fn(), + }; + const mockTokenManager: jest.Mocked = { + getToken: jest.fn().mockResolvedValue({ token: '' }), + authenticate: jest.fn(), + }; + const options = { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + }; + + beforeAll(() => { + server.listen(); + }); + + beforeEach(() => { + server.use( + rest.get('http://localhost:7007/entities', (req, res, ctx) => { + if (req.url.searchParams.has('filter')) { + const filter = req.url.searchParams.get('filter'); + if (filter === 'kind=Foo,kind=Bar') { + // When filtering on the 'Foo,Bar' kinds we simply return no items, to simulate a filter + return res(ctx.json([])); + } + throw new Error('Unexpected filter parameter'); + } + + // Imitate offset/limit pagination. + const offset = parseInt(req.url.searchParams.get('offset') || '0', 10); + const limit = parseInt(req.url.searchParams.get('limit') || '500', 10); + return res(ctx.json(expectedEntities.slice(offset, limit + offset))); + }), + ); + }); + + afterAll(() => { + server.close(); + }); + + afterEach(() => server.resetHandlers()); + + it('has expected type', () => { + const factory = DefaultCatalogCollatorFactory.fromConfig(config, options); + expect(factory.type).toBe('software-catalog'); + }); + + describe('getCollator', () => { + let factory: DefaultCatalogCollatorFactory; + let collator: Readable; + + beforeEach(async () => { + factory = DefaultCatalogCollatorFactory.fromConfig(config, options); + collator = await factory.getCollator(); + }); + + it('returns a readable stream', async () => { + expect(collator).toBeInstanceOf(Readable); + }); + + it('fetches from the configured catalog service', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog'); + expect(documents).toHaveLength(expectedEntities.length); + }); + + it('maps a returned entity to an expected CatalogEntityDocument', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents[0]).toMatchObject({ + title: expectedEntities[0].metadata.name, + location: '/catalog/default/component/test-entity', + text: expectedEntities[0].metadata.description, + namespace: 'default', + componentType: expectedEntities[0]!.spec!.type, + lifecycle: expectedEntities[0]!.spec!.lifecycle, + owner: expectedEntities[0]!.spec!.owner, + authorization: { + resourceRef: 'component:default/test-entity', + }, + }); + expect(documents[1]).toMatchObject({ + title: expectedEntities[1].metadata.title, + location: '/catalog/default/component/test-entity-2', + text: expectedEntities[1].metadata.description, + namespace: 'default', + componentType: expectedEntities[1]!.spec!.type, + lifecycle: expectedEntities[1]!.spec!.lifecycle, + owner: expectedEntities[1]!.spec!.owner, + authorization: { + resourceRef: 'component:default/test-entity-2', + }, + }); + }); + + it('maps a returned entity with a custom locationTemplate', async () => { + // Provide an alternate location template. + factory = DefaultCatalogCollatorFactory.fromConfig(new ConfigReader({}), { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + locationTemplate: '/software/:name', + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + expect(documents[0]).toMatchObject({ + location: '/software/test-entity', + }); + }); + + it('allows filtering of the retrieved catalog entities', async () => { + // Provide a custom filter. + factory = DefaultCatalogCollatorFactory.fromConfig(new ConfigReader({}), { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + filter: { + kind: ['Foo', 'Bar'], + }, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + // The simulated 'Foo,Bar' filter should return in an empty list + expect(documents).toHaveLength(0); + }); + + it('paginates through catalog entities using batchSize', async () => { + factory = DefaultCatalogCollatorFactory.fromConfig(config, { + ...options, + batchSize: 1, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents).toHaveLength(expectedEntities.length); + expect(documents[0].location).toBe( + '/catalog/default/component/test-entity', + ); + expect(documents[1].location).toBe( + '/catalog/default/component/test-entity-2', + ); + }); + }); +}); diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts new file mode 100644 index 0000000000..99377b2ca0 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -0,0 +1,173 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { + Entity, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + DocumentCollatorFactory, + IndexableDocument, +} from '@backstage/search-common'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; +import { Readable } from 'stream'; + +/** @public */ +export interface CatalogEntityDocument extends IndexableDocument { + componentType: string; + namespace: string; + kind: string; + lifecycle: string; + owner: string; +} + +/** @public */ +export type DefaultCatalogCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + +/** @public */ +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'software-catalog'; + public readonly visibilityPermission = catalogEntityReadPermission; + + private locationTemplate: string; + private filter?: GetEntitiesRequest['filter']; + private batchSize: number; + private readonly catalogClient: CatalogApi; + private tokenManager: TokenManager; + + static fromConfig( + _config: Config, + options: DefaultCatalogCollatorFactoryOptions, + ) { + return new DefaultCatalogCollatorFactory(options); + } + + private constructor(options: DefaultCatalogCollatorFactoryOptions) { + const { + batchSize, + discovery, + locationTemplate, + filter, + catalogClient, + tokenManager, + } = options; + + this.locationTemplate = + locationTemplate || '/catalog/:namespace/:kind/:name'; + this.filter = filter; + this.batchSize = batchSize || 500; + this.catalogClient = + catalogClient || new CatalogClient({ discoveryApi: discovery }); + this.tokenManager = tokenManager; + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + return formatted.toLowerCase(); + } + + private isUserEntity(entity: Entity): entity is UserEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'USER'; + } + + private getDocumentText(entity: Entity): string { + let documentText = entity.metadata.description || ''; + if (this.isUserEntity(entity)) { + if (entity.spec?.profile?.displayName && documentText) { + // combine displayName and description + const displayName = entity.spec?.profile?.displayName; + documentText = displayName.concat(' : ', documentText); + } else { + documentText = entity.spec?.profile?.displayName || documentText; + } + } + return documentText; + } + + private async *execute(): AsyncGenerator { + const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; + let moreEntitiesToGet = true; + + // Offset/limit pagination is used on the Catalog Client in order to + // limit (and allow some control over) memory used by the search backend + // at index-time. + while (moreEntitiesToGet) { + const entities = ( + await this.catalogClient.getEntities( + { + filter: this.filter, + limit: this.batchSize, + offset: entitiesRetrieved, + }, + { token }, + ) + ).items; + + // Control looping through entity batches. + moreEntitiesToGet = entities.length === this.batchSize; + entitiesRetrieved += entities.length; + + for (const entity of entities) { + yield { + title: entity.metadata.title ?? entity.metadata.name, + location: this.applyArgsToFormat(this.locationTemplate, { + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + name: entity.metadata.name, + }), + text: this.getDocumentText(entity), + componentType: entity.spec?.type?.toString() || 'other', + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: (entity.spec?.owner as string) || '', + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + }; + } + } + } +} diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index 5641670333..93ff0b8b32 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -14,5 +14,11 @@ * limitations under the License. */ +export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; +export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; +export type { CatalogEntityDocument } from './DefaultCatalogCollatorFactory'; + +/** + * todo(backstage/techdocs-core): stop exporting this in a future release. + */ export { DefaultCatalogCollator } from './DefaultCatalogCollator'; -export type { CatalogEntityDocument } from './DefaultCatalogCollator'; diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts index 156db8e8ce..1f0e5ea644 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts @@ -30,6 +30,7 @@ describe('AuthorizedEntitiesCatalog', () => { entities: jest.fn(), removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), + facets: jest.fn(), }; const fakePermissionApi = { authorize: jest.fn(), @@ -254,4 +255,54 @@ describe('AuthorizedEntitiesCatalog', () => { }); }); }); + + describe('facets', () => { + it('returns empty response on DENY', async () => { + fakePermissionApi.authorize.mockResolvedValue([ + { result: AuthorizeResult.DENY }, + ]); + const catalog = createCatalog(); + + expect( + await catalog.facets({ + facets: ['a'], + authorizationToken: 'abcd', + }), + ).toEqual({ + facets: { a: [] }, + }); + }); + + it('calls underlying catalog method with correct filter on CONDITIONAL', async () => { + fakePermissionApi.authorize.mockResolvedValue([ + { + result: AuthorizeResult.CONDITIONAL, + conditions: { rule: 'IS_ENTITY_KIND', params: [['b']] }, + }, + ]); + const catalog = createCatalog(isEntityKind); + + await catalog.facets({ facets: ['a'], authorizationToken: 'abcd' }); + + expect(fakeCatalog.facets).toHaveBeenCalledWith({ + facets: ['a'], + authorizationToken: 'abcd', + filter: { key: 'kind', values: ['b'] }, + }); + }); + + it('calls underlying catalog method on ALLOW', async () => { + fakePermissionApi.authorize.mockResolvedValue([ + { result: AuthorizeResult.ALLOW }, + ]); + const catalog = createCatalog(); + + await catalog.facets({ facets: ['a'], authorizationToken: 'abcd' }); + + expect(fakeCatalog.facets).toHaveBeenCalledWith({ + facets: ['a'], + authorizationToken: 'abcd', + }); + }); + }); }); diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts index 99fc3ebc8e..2f94e23303 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts @@ -30,6 +30,8 @@ import { EntitiesRequest, EntitiesResponse, EntityAncestryResponse, + EntityFacetsRequest, + EntityFacetsResponse, EntityFilter, } from '../catalog/types'; import { basicEntityFilter } from './request/basicEntityFilter'; @@ -151,6 +153,35 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { }; } + async facets(request: EntityFacetsRequest): Promise { + const authorizeDecision = ( + await this.permissionApi.authorize( + [{ permission: catalogEntityReadPermission }], + { token: request?.authorizationToken }, + ) + )[0]; + + if (authorizeDecision.result === AuthorizeResult.DENY) { + return { + facets: Object.fromEntries(request.facets.map(f => [f, []])), + }; + } + + if (authorizeDecision.result === AuthorizeResult.CONDITIONAL) { + const permissionFilter: EntityFilter = this.transformConditions( + authorizeDecision.conditions, + ); + return this.entitiesCatalog.facets({ + ...request, + filter: request?.filter + ? { allOf: [permissionFilter, request.filter] } + : permissionFilter, + }); + } + + return this.entitiesCatalog.facets(request); + } + private findParents( entityRef: string, allAncestryItems: { entity: Entity; parentEntityRefs: string[] }[], diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts index 1ae2833cd0..077f56c737 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 { Entity } from '@backstage/catalog-model'; import { NotAllowedError, NotFoundError } from '@backstage/errors'; import { catalogLocationCreatePermission, @@ -25,7 +26,7 @@ import { AuthorizeResult, PermissionAuthorizer, } from '@backstage/plugin-permission-common'; -import { LocationService } from './types'; +import { LocationInput, LocationService } from './types'; export class AuthorizedLocationService implements LocationService { constructor( @@ -34,7 +35,7 @@ export class AuthorizedLocationService implements LocationService { ) {} async createLocation( - spec: LocationSpec, + spec: LocationInput, dryRun: boolean, options?: { authorizationToken?: string; diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts index 819451d854..17dfb58c54 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { NotAllowedError } from '@backstage/errors'; import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common'; import { diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 46c92c43da..f62377decb 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -38,12 +38,15 @@ import { Router } from 'express'; import lodash, { keyBy } from 'lodash'; import { EntitiesCatalog, EntitiesSearchFilter } from '../catalog'; +import { + CatalogProcessor, + CatalogProcessorParser, + EntityProvider, +} from '../api'; import { AnnotateLocationEntityProcessor, BitbucketDiscoveryProcessor, BuiltinKindsEntityProcessor, - CatalogProcessor, - CatalogProcessorParser, CodeOwnersProcessor, FileReaderProcessor, AzureDevOpsDiscoveryProcessor, @@ -53,23 +56,22 @@ import { PlaceholderProcessor, PlaceholderResolver, UrlReaderProcessor, -} from '../ingestion'; +} from '../modules'; +import { ConfigLocationEntityProvider } from '../modules/core/ConfigLocationEntityProvider'; +import { DefaultLocationStore } from '../modules/core/DefaultLocationStore'; import { RepoLocationAnalyzer } from '../ingestion/LocationAnalyzer'; import { jsonPlaceholderResolver, textPlaceholderResolver, yamlPlaceholderResolver, -} from '../ingestion/processors/PlaceholderProcessor'; -import { defaultEntityDataParser } from '../ingestion/processors/util/parse'; +} from '../modules/core/PlaceholderProcessor'; +import { defaultEntityDataParser } from '../modules/util/parse'; import { LocationAnalyzer } from '../ingestion/types'; -import { EntityProvider } from '../providers/types'; import { CatalogProcessingEngine } from '../processing/types'; -import { ConfigLocationEntityProvider } from '../providers/ConfigLocationEntityProvider'; import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase'; import { applyDatabaseMigrations } from '../database/migrations'; import { DefaultCatalogProcessingEngine } from '../processing/DefaultCatalogProcessingEngine'; import { DefaultLocationService } from './DefaultLocationService'; -import { DefaultLocationStore } from '../providers/DefaultLocationStore'; import { DefaultEntitiesCatalog } from './DefaultEntitiesCatalog'; import { DefaultCatalogProcessingOrchestrator } from '../processing/DefaultCatalogProcessingOrchestrator'; import { Stitcher } from '../stitching/Stitcher'; @@ -97,6 +99,7 @@ import { basicEntityFilter } from './request/basicEntityFilter'; import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; import { AuthorizedLocationService } from './AuthorizedLocationService'; +/** @public */ export type CatalogEnvironment = { logger: Logger; database: PluginDatabaseManager; diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index d6d4e957f6..43e208197f 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -469,6 +469,62 @@ describe('DefaultEntitiesCatalog', () => { expect(entities.length).toBe(0); }, ); + + it.each(databases.eachSupportedId())( + 'should return both target and targetRef for entities', + async databaseId => { + const { knex } = await createDatabase(databaseId); + await addEntity( + knex, + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'one' }, + spec: {}, + relations: [{ type: 'r', targetRef: 'x:y/z' } as any], + }, + [], + ); + await addEntity( + knex, + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'two' }, + spec: {}, + relations: [ + { + type: 'r', + target: { kind: 'x', namespace: 'y', name: 'z' }, + } as any, + ], + }, + [], + ); + const catalog = new DefaultEntitiesCatalog(knex); + + const { entities } = await catalog.entities(); + + expect( + entities.find(e => e.metadata.name === 'one')!.relations, + ).toEqual([ + { + type: 'r', + targetRef: 'x:y/z', + target: { kind: 'x', namespace: 'y', name: 'z' }, + }, + ]); + expect( + entities.find(e => e.metadata.name === 'two')!.relations, + ).toEqual([ + { + type: 'r', + targetRef: 'x:y/z', + target: { kind: 'x', namespace: 'y', name: 'z' }, + }, + ]); + }, + ); }); describe('removeEntityByUid', () => { @@ -535,4 +591,129 @@ describe('DefaultEntitiesCatalog', () => { }, ); }); + + describe('facets', () => { + it.each(databases.eachSupportedId())( + 'can filter and collect properly', + async databaseId => { + const { knex } = await createDatabase(databaseId); + + await addEntityToSearch(knex, { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'one' }, + spec: {}, + }); + await addEntityToSearch(knex, { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'two' }, + spec: {}, + }); + await addEntityToSearch(knex, { + apiVersion: 'a', + kind: 'k2', + metadata: { name: 'two' }, + spec: {}, + }); + const catalog = new DefaultEntitiesCatalog(knex); + + await expect(catalog.facets({ facets: ['kind'] })).resolves.toEqual({ + facets: { + kind: [ + { value: 'k', count: 2 }, + { value: 'k2', count: 1 }, + ], + }, + }); + }, + ); + + it.each(databases.eachSupportedId())( + 'can match on annotations and labels with dots in them', + async databaseId => { + const { knex } = await createDatabase(databaseId); + + await addEntityToSearch(knex, { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'one', + annotations: { 'a.b/c.d': 'annotation1' }, + labels: { 'e.f/g.h': 'label1' }, + }, + spec: {}, + }); + await addEntityToSearch(knex, { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'two', + annotations: { 'a.b/c.d': 'annotation2' }, + labels: { 'e.f/g.h': 'label2' }, + }, + spec: {}, + }); + const catalog = new DefaultEntitiesCatalog(knex); + + await expect( + catalog.facets({ + facets: ['metadata.annotations.a.b/c.d', 'metadata.labels.e.f/g.h'], + }), + ).resolves.toEqual({ + facets: { + 'metadata.annotations.a.b/c.d': [ + { value: 'annotation1', count: 1 }, + { value: 'annotation2', count: 1 }, + ], + 'metadata.labels.e.f/g.h': [ + { value: 'label1', count: 1 }, + { value: 'label2', count: 1 }, + ], + }, + }); + }, + ); + + it.each(databases.eachSupportedId())( + 'can match on strings in arrays', + async databaseId => { + const { knex } = await createDatabase(databaseId); + + await addEntityToSearch(knex, { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'one', + tags: ['java', 'rust'], + }, + spec: {}, + }); + await addEntityToSearch(knex, { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'two', + tags: ['java', 'node'], + }, + spec: {}, + }); + const catalog = new DefaultEntitiesCatalog(knex); + + await expect( + catalog.facets({ + facets: ['metadata.tags'], + }), + ).resolves.toEqual({ + facets: { + 'metadata.tags': expect.arrayContaining([ + { value: 'java', count: 2 }, + { value: 'rust', count: 1 }, + { value: 'node', count: 1 }, + ]), + }, + }); + }, + ); + }); }); diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index 6979f94ba7..f8b68bebc5 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -14,24 +14,31 @@ * limitations under the License. */ -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { + Entity, + parseEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { InputError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; +import lodash from 'lodash'; import { EntitiesCatalog, EntitiesRequest, EntitiesResponse, - EntityAncestryResponse, - EntityPagination, - EntityFilter, EntitiesSearchFilter, + EntityAncestryResponse, + EntityFacetsRequest, + EntityFacetsResponse, + EntityFilter, + EntityPagination, } from '../catalog/types'; import { DbFinalEntitiesRow, + DbPageInfo, DbRefreshStateReferencesRow, DbRefreshStateRow, DbSearchRow, - DbPageInfo, } from '../database/tables'; function parsePagination(input?: EntityPagination): { @@ -191,11 +198,32 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { }; } - const dbResponse = rows.map(e => JSON.parse(e.final_entity!)); + let entities: Entity[] = rows.map(e => JSON.parse(e.final_entity!)); - const entities = dbResponse.map(e => - request?.fields ? request.fields(e) : e, - ); + if (request?.fields) { + entities = entities.map(e => request.fields!(e)); + } + + // TODO(freben): This is added as a compatibility guarantee, until we can be + // sure that all adopters have re-stitched their entities so that the new + // targetRef field is present on them, and that they have stopped consuming + // the now-removed old field + for (const entity of entities) { + if (entity.relations) { + for (const relation of entity.relations) { + if (!relation.targetRef && relation.target) { + // This is the case where an old-form entity, not yet stitched with + // the updated code, was in the database + relation.targetRef = stringifyEntityRef(relation.target); + } else if (!relation.target && relation.targetRef) { + // This is the case where a new-form entity, stitched with the + // updated code, was in the database but we still want to produce + // the old data shape as well for compatibility reasons + relation.target = parseEntityRef(relation.targetRef); + } + } + } + } return { entities, @@ -295,4 +323,49 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { items, }; } + + async facets(request: EntityFacetsRequest): Promise { + const { entities } = await this.entities({ + filter: request.filter, + authorizationToken: request.authorizationToken, + }); + + const facets: EntityFacetsResponse['facets'] = {}; + + for (const facet of request.facets) { + const values = entities + .map(entity => { + // TODO(freben): Generalize this code to handle any field that may + // have dots in its key? + if (facet.startsWith('metadata.annotations.')) { + return entity.metadata.annotations?.[ + facet.substring('metadata.annotations.'.length) + ]; + } else if (facet.startsWith('metadata.labels.')) { + return entity.metadata.labels?.[ + facet.substring('metadata.labels.'.length) + ]; + } + return lodash.get(entity, facet); + }) + .flatMap(field => { + if (typeof field === 'string') { + return [field]; + } else if (Array.isArray(field)) { + return field.filter(i => typeof i === 'string'); + } + return []; + }) + .sort(); + + const counts = lodash.countBy(values, lodash.identity); + + facets[facet] = Object.entries(counts).map(([value, count]) => ({ + value, + count, + })); + } + + return { facets }; + } } diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index 8dd2274842..7a1b923068 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -13,19 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + 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, } from '../processing/types'; -import { LocationService, LocationStore } from './types'; +import { LocationInput, LocationService, LocationStore } from './types'; import { locationSpecToMetadataName } from '../util/conversion'; export class DefaultLocationService implements LocationService { @@ -35,13 +35,13 @@ export class DefaultLocationService implements LocationService { ) {} async createLocation( - spec: LocationSpec, + input: LocationInput, dryRun: boolean, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }> { if (dryRun) { - return this.dryRunCreateLocation(spec); + return this.dryRunCreateLocation(input); } - const location = await this.store.createLocation(spec); + const location = await this.store.createLocation(input); return { location, entities: [] }; } @@ -93,7 +93,7 @@ export class DefaultLocationService implements LocationService { } private async dryRunCreateLocation( - spec: LocationSpec, + spec: LocationInput, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }> { // Run the existence check in parallel with the processing const existsPromise = this.store @@ -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/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 205be01b42..8db15cb4a1 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -17,11 +17,12 @@ 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 } from '@backstage/catalog-model'; import express from 'express'; import request from 'supertest'; import { EntitiesCatalog } from '../catalog'; -import { LocationService, RefreshService } from './types'; +import { LocationInput, LocationService, RefreshService } from './types'; import { basicEntityFilter } from './request'; import { createRouter } from './createRouter'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; @@ -39,6 +40,7 @@ describe('createRouter readonly disabled', () => { entities: jest.fn(), removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), + facets: jest.fn(), }; locationService = { getLocation: jest.fn(), @@ -297,7 +299,7 @@ describe('createRouter readonly disabled', () => { const spec = { typez: 'b', target: 'c', - } as unknown as LocationSpec; + } as unknown as LocationInput; const response = await request(app) .post('/locations') @@ -309,7 +311,7 @@ describe('createRouter readonly disabled', () => { }); it('passes the body down', async () => { - const spec: LocationSpec = { + const spec: LocationInput = { type: 'b', target: 'c', }; @@ -337,7 +339,7 @@ describe('createRouter readonly disabled', () => { }); it('supports dry run', async () => { - const spec: LocationSpec = { + const spec: LocationInput = { type: 'b', target: 'c', }; @@ -393,6 +395,7 @@ describe('createRouter readonly enabled', () => { entities: jest.fn(), removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), + facets: jest.fn(), }; locationService = { getLocation: jest.fn(), @@ -503,7 +506,7 @@ describe('createRouter readonly enabled', () => { describe('POST /locations', () => { it('is not allowed', async () => { - const spec: LocationSpec = { + const spec: LocationInput = { type: 'b', target: 'c', }; @@ -519,7 +522,7 @@ describe('createRouter readonly enabled', () => { }); it('supports dry run', async () => { - const spec: LocationSpec = { + const spec: LocationInput = { type: 'b', target: 'c', }; @@ -577,6 +580,7 @@ describe('NextRouter permissioning', () => { entities: jest.fn(), removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), + facets: jest.fn(), }; locationService = { getLocation: jest.fn(), diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 0dce765507..56121f4155 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -32,11 +32,12 @@ import { } from './request'; import { disallowReadonlyMode, - locationSpec, + locationInput, validateRequestBody, } from './util'; import { RefreshOptions, LocationService, RefreshService } from './types'; import { z } from 'zod'; +import { parseEntityFacetParams } from './request/parseEntityFacetParams'; /** * Options used by {@link createRouter}. @@ -160,13 +161,21 @@ export async function createRouter( const response = await entitiesCatalog.entityAncestry(entityRef); res.status(200).json(response); }, - ); + ) + .get('/entity-facets', async (req, res) => { + const response = await entitiesCatalog.facets({ + filter: parseEntityFilterParams(req.query), + facets: parseEntityFacetParams(req.query), + authorizationToken: getBearerToken(req.header('authorization')), + }); + res.status(200).json(response); + }); } if (locationService) { router .post('/locations', async (req, res) => { - const location = await validateRequestBody(req, locationSpec); + const location = await validateRequestBody(req, locationInput); const dryRun = yn(req.query.dryRun, { default: false }); // when in dryRun addLocation is effectively a read operation so we don't @@ -209,9 +218,9 @@ export async function createRouter( router.post('/analyze-location', async (req, res) => { const body = await validateRequestBody( req, - z.object({ location: locationSpec }), + z.object({ location: locationInput }), ); - const schema = z.object({ location: locationSpec }); + const schema = z.object({ location: locationInput }); const output = await locationAnalyzer.analyzeLocation(schema.parse(body)); res.status(200).json(output); }); diff --git a/plugins/catalog-backend/src/service/index.ts b/plugins/catalog-backend/src/service/index.ts index fd1b31b9d1..e1c7f54475 100644 --- a/plugins/catalog-backend/src/service/index.ts +++ b/plugins/catalog-backend/src/service/index.ts @@ -19,6 +19,7 @@ export type { RefreshService, RefreshOptions, LocationStore, + LocationInput, } from './types'; export { createRouter } from './createRouter'; export type { RouterOptions } from './createRouter'; diff --git a/plugins/catalog-backend/src/service/request/parseEntityFacetParams.ts b/plugins/catalog-backend/src/service/request/parseEntityFacetParams.ts new file mode 100644 index 0000000000..00493f068f --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseEntityFacetParams.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { parseStringsParam } from './common'; + +/** + * Parses the facets part of a facet query, like + * /entity-facets?filter=metadata.namespace=default,kind=Component&facet=metadata.namespace + */ +export function parseEntityFacetParams( + params: Record, +): string[] { + // Each facet string is on the form a.b.c + const facetStrings = parseStringsParam(params.facet, 'facet'); + if (facetStrings) { + const filtered = facetStrings.filter(Boolean); + if (filtered.length) { + return filtered; + } + } + + throw new InputError('Missing facet parameter'); +} diff --git a/plugins/catalog-backend/src/service/types.ts b/plugins/catalog-backend/src/service/types.ts index 02d234b6a8..02268ab63c 100644 --- a/plugins/catalog-backend/src/service/types.ts +++ b/plugins/catalog-backend/src/service/types.ts @@ -14,11 +14,28 @@ * limitations under the License. */ -import { Entity, LocationSpec, Location } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; +import { Location } from '@backstage/catalog-client'; +/** + * Holds the information required to create a new location in the catalog location store. + * + * @public + */ +export interface LocationInput { + type: string; + target: string; + /** @deprecated This field is ignored and will be removed */ + presence?: 'optional' | 'required'; +} + +/** + * The location service manages entity locations. + * @public + */ export interface LocationService { createLocation( - spec: LocationSpec, + location: LocationInput, dryRun: boolean, options?: { authorizationToken?: string; @@ -58,8 +75,11 @@ export interface RefreshService { refresh(options: RefreshOptions): Promise; } +/** + * Interacts with the database to manage locations. + * @public */ export interface LocationStore { - createLocation(spec: LocationSpec): Promise; + createLocation(location: LocationInput): Promise; listLocations(): Promise; getLocation(id: string): Promise; deleteLocation(id: string): Promise; diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 09f1c2d8cf..9a914aadcb 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -40,7 +40,7 @@ export async function requireRequestBody(req: Request): Promise { return body; } -export const locationSpec = z +export const locationInput = z .object({ type: z.string(), target: z.string(), diff --git a/plugins/catalog-backend/src/stitching/Stitcher.test.ts b/plugins/catalog-backend/src/stitching/Stitcher.test.ts index be57ff7083..9df4bedd60 100644 --- a/plugins/catalog-backend/src/stitching/Stitcher.test.ts +++ b/plugins/catalog-backend/src/stitching/Stitcher.test.ts @@ -93,6 +93,7 @@ describe('Stitcher', () => { relations: [ { type: 'looksAt', + targetRef: 'k:ns/other', target: { kind: 'k', namespace: 'ns', @@ -159,6 +160,7 @@ describe('Stitcher', () => { relations: expect.arrayContaining([ { type: 'looksAt', + targetRef: 'k:ns/other', target: { kind: 'k', namespace: 'ns', @@ -167,6 +169,7 @@ describe('Stitcher', () => { }, { type: 'looksAt', + targetRef: 'k:ns/third', target: { kind: 'k', namespace: 'ns', diff --git a/plugins/catalog-backend/src/stitching/Stitcher.ts b/plugins/catalog-backend/src/stitching/Stitcher.ts index 9ec028ed35..37b86fef46 100644 --- a/plugins/catalog-backend/src/stitching/Stitcher.ts +++ b/plugins/catalog-backend/src/stitching/Stitcher.ts @@ -18,6 +18,7 @@ import { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client import { AlphaEntity, parseEntityRef, + EntityRelation, EntityStatusItem, } from '@backstage/catalog-model'; import { SerializedError, stringifyError } from '@backstage/errors'; @@ -183,9 +184,11 @@ export class Stitcher { ); entity.relations = uniqueRelationRows .filter(row => row.relationType /* exclude null row, if relevant */) - .map(row => ({ + .map(row => ({ type: row.relationType!, + // TODO(freben): This field is deprecated and should be removed in a future release target: parseEntityRef(row.relationTarget!), + targetRef: row.relationTarget!, })); if (statusItems.length) { entity.status = { @@ -210,7 +213,7 @@ export class Stitcher { } // This may throw if the entity is invalid, so we call it before - // the final_entites write, even though we may end up not needing + // the final_entities write, even though we may end up not needing // to write the search index. const searchEntries = buildEntitySearch(entityId, entity); diff --git a/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts b/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts index 58551a461f..52e3392676 100644 --- a/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts +++ b/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { buildEntitySearch, mapToRows, traverse } from './buildEntitySearch'; describe('buildEntitySearch', () => { @@ -133,7 +133,7 @@ describe('buildEntitySearch', () => { { entity_id: 'eid', key: 'metadata.namespace', - value: ENTITY_DEFAULT_NAMESPACE, + value: DEFAULT_NAMESPACE, }, ]); }); @@ -141,8 +141,16 @@ describe('buildEntitySearch', () => { it('adds relations', () => { const input: Entity = { relations: [ - { type: 't1', target: { kind: 'k', namespace: 'ns', name: 'a' } }, - { type: 't2', target: { kind: 'k', namespace: 'ns', name: 'b' } }, + { + type: 't1', + targetRef: 'k:ns/a', + target: { kind: 'k', namespace: 'ns', name: 'a' }, + }, + { + type: 't2', + targetRef: 'k:ns/b', + target: { kind: 'k', namespace: 'ns', name: 'b' }, + }, ], apiVersion: 'a', kind: 'b', @@ -157,7 +165,7 @@ describe('buildEntitySearch', () => { { entity_id: 'eid', key: 'metadata.namespace', - value: ENTITY_DEFAULT_NAMESPACE, + value: DEFAULT_NAMESPACE, }, { entity_id: 'eid', key: 'relations.t1', value: 'k:ns/a' }, { entity_id: 'eid', key: 'relations.t2', value: 'k:ns/b' }, @@ -182,8 +190,16 @@ describe('buildEntitySearch', () => { expect(() => buildEntitySearch('eid', { relations: [ - { type: 'dup', target: { kind: 'k', namespace: 'ns', name: 'a' } }, - { type: 'DUP', target: { kind: 'k', namespace: 'ns', name: 'b' } }, + { + type: 'dup', + targetRef: 'k:ns/a', + target: { kind: 'k', namespace: 'ns', name: 'a' }, + }, + { + type: 'DUP', + targetRef: 'k:ns/b', + target: { kind: 'k', namespace: 'ns', name: 'b' }, + }, ], apiVersion: 'a', kind: 'b', diff --git a/plugins/catalog-backend/src/stitching/buildEntitySearch.ts b/plugins/catalog-backend/src/stitching/buildEntitySearch.ts index 84c6036013..cd8a3735de 100644 --- a/plugins/catalog-backend/src/stitching/buildEntitySearch.ts +++ b/plugins/catalog-backend/src/stitching/buildEntitySearch.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - Entity, - ENTITY_DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { InputError } from '@backstage/errors'; import { DbSearchRow } from '../database/tables'; @@ -174,14 +170,14 @@ export function buildEntitySearch( // Namespace not specified has the default value "default", so we want to // match on that as well if (!entity.metadata.namespace) { - raw.push({ key: 'metadata.namespace', value: ENTITY_DEFAULT_NAMESPACE }); + raw.push({ key: 'metadata.namespace', value: DEFAULT_NAMESPACE }); } // Visit relations for (const relation of entity.relations ?? []) { raw.push({ key: `relations.${relation.type}`, - value: stringifyEntityRef(relation.target), + value: relation.targetRef, }); } diff --git a/plugins/catalog-backend/src/util/RecursivePartial.ts b/plugins/catalog-backend/src/util/RecursivePartial.ts index 502f4464c1..92e1ab9296 100644 --- a/plugins/catalog-backend/src/util/RecursivePartial.ts +++ b/plugins/catalog-backend/src/util/RecursivePartial.ts @@ -16,6 +16,7 @@ /** * Makes all keys of an entire hierarchy optional. + * @public */ export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] diff --git a/plugins/catalog-backend/src/util/conversion.ts b/plugins/catalog-backend/src/util/conversion.ts index 559a472c25..63771d1028 100644 --- a/plugins/catalog-backend/src/util/conversion.ts +++ b/plugins/catalog-backend/src/util/conversion.ts @@ -17,13 +17,13 @@ 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'; +import { LocationSpec } from '../api'; export function locationSpecToMetadataName(location: LocationSpec) { const hash = createHash('sha1') @@ -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-backend/src/util/index.ts b/plugins/catalog-backend/src/util/index.ts index 2e592fb8df..13a9c84b0f 100644 --- a/plugins/catalog-backend/src/util/index.ts +++ b/plugins/catalog-backend/src/util/index.ts @@ -16,4 +16,3 @@ export * from './runPeriodically'; export * from './RecursivePartial'; -export * from './timing'; diff --git a/plugins/catalog-backend/src/util/runPeriodically.ts b/plugins/catalog-backend/src/util/runPeriodically.ts index 2f3104e221..361fb33b2f 100644 --- a/plugins/catalog-backend/src/util/runPeriodically.ts +++ b/plugins/catalog-backend/src/util/runPeriodically.ts @@ -23,6 +23,8 @@ * @param delayMs - The delay between a completed function invocation and the * next. * @returns A function that, when called, stops the invocation loop. + * @deprecated use \@backstage/backend-tasks package instead. + * @public */ export function runPeriodically(fn: () => any, delayMs: number): () => void { let cancel: () => void; diff --git a/plugins/catalog-backend/src/util/timing.ts b/plugins/catalog-backend/src/util/timing.ts deleted file mode 100644 index b5f9eb258f..0000000000 --- a/plugins/catalog-backend/src/util/timing.ts +++ /dev/null @@ -1,31 +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. - */ - -/** - * Returns a string with the elapsed time since the start of an operation, - * with some human friendly precision, e.g. "133ms" or "14.5s". - * - * @param startTimestamp - The timestamp (from process.hrtime()) at the start ot - * the operation - */ -export function durationText(startTimestamp: [number, number]): string { - const delta = process.hrtime(startTimestamp); - const seconds = delta[0] + delta[1] / 1e9; - if (seconds > 1) { - return `${seconds.toFixed(1)}s`; - } - return `${(seconds * 1000).toFixed(0)}ms`; -} diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index a236e55857..337bd6854c 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-common +## 0.1.4 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/plugin-permission-common@0.5.1 + +## 0.1.3 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/plugin-permission-common@0.5.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/catalog-common/api-report.md b/plugins/catalog-common/api-report.md index 9826a8ddef..57f8c3983e 100644 --- a/plugins/catalog-common/api-report.md +++ b/plugins/catalog-common/api-report.md @@ -5,27 +5,27 @@ ```ts import { Permission } from '@backstage/plugin-permission-common'; -// @public +// @alpha export const catalogEntityCreatePermission: Permission; -// @public +// @alpha export const catalogEntityDeletePermission: Permission; -// @public +// @alpha export const catalogEntityReadPermission: Permission; -// @public +// @alpha export const catalogEntityRefreshPermission: Permission; -// @public +// @alpha export const catalogLocationCreatePermission: Permission; -// @public +// @alpha export const catalogLocationDeletePermission: Permission; -// @public +// @alpha export const catalogLocationReadPermission: Permission; -// @public (undocumented) +// @alpha export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; ``` diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 4b94809ed5..acf4ed4aaa 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.2", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,7 +10,11 @@ "access": "public", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" + }, + "backstage": { + "role": "common-library" }, "homepage": "https://backstage.io", "repository": { @@ -22,20 +26,21 @@ "backstage" ], "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" + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-permission-common": "^0.4.0" + "@backstage/plugin-permission-common": "^0.5.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0" + "@backstage/cli": "^0.14.0" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/catalog-common/src/permissions.ts b/plugins/catalog-common/src/permissions.ts index 52c225e441..31a8f32118 100644 --- a/plugins/catalog-common/src/permissions.ts +++ b/plugins/catalog-common/src/permissions.ts @@ -17,8 +17,10 @@ import { Permission } from '@backstage/plugin-permission-common'; /** + * Permission resource type which corresponds to catalog entities. + * * {@link https://backstage.io/docs/features/software-catalog/software-catalog-overview} - * @public + * @alpha */ export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; @@ -28,7 +30,7 @@ export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; * * If this permission is not authorized, it will appear that the entity does not * exist in the catalog — both in the frontend and in API responses. - * @public + * @alpha */ export const catalogEntityReadPermission: Permission = { name: 'catalog.entity.read', @@ -42,20 +44,19 @@ export const catalogEntityReadPermission: Permission = { * This permission is used to authorize actions that involve creating a new * catalog entity. This includes registering an existing component into the * catalog. - * @public + * @alpha */ 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. - * @public + * @alpha */ export const catalogEntityDeletePermission: Permission = { name: 'catalog.entity.delete', @@ -68,7 +69,7 @@ export const catalogEntityDeletePermission: Permission = { /** * This permission is used to designate refreshing one or more entities from the * catalog. - * @public + * @alpha */ export const catalogEntityRefreshPermission: Permission = { name: 'catalog.entity.refresh', @@ -84,7 +85,7 @@ export const catalogEntityRefreshPermission: Permission = { * * If this permission is not authorized, it will appear that the location does * not exist in the catalog — both in the frontend and in API responses. - * @public + * @alpha */ export const catalogLocationReadPermission: Permission = { name: 'catalog.location.read', @@ -96,7 +97,7 @@ export const catalogLocationReadPermission: Permission = { /** * This permission is used to designate actions that involve creating catalog * locations. - * @public + * @alpha */ export const catalogLocationCreatePermission: Permission = { name: 'catalog.location.create', @@ -108,7 +109,7 @@ export const catalogLocationCreatePermission: Permission = { /** * This permission is used to designate actions that involve deleting locations * from the catalog. - * @public + * @alpha */ export const catalogLocationDeletePermission: Permission = { name: 'catalog.location.delete', diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 0bb6e7224a..5ce044aa14 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-catalog-graph +## 0.2.12 + +### Patch Changes + +- a8331830f1: Remove use of deprecated `useEntityKinds` hook. +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.11 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 538ca90790: Use updated type names from `@backstage/catalog-client` +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/catalog-client@0.7.0 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.2.10 ### Patch Changes diff --git a/plugins/catalog-graph/dev/index.tsx b/plugins/catalog-graph/dev/index.tsx index ed14e5fce9..62d5eccef3 100644 --- a/plugins/catalog-graph/dev/index.tsx +++ b/plugins/catalog-graph/dev/index.tsx @@ -13,11 +13,11 @@ * 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, - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, RELATION_API_CONSUMED_BY, RELATION_API_PROVIDED_BY, RELATION_CONSUMES_API, @@ -42,6 +42,7 @@ import { catalogGraphPlugin, EntityCatalogGraphCard, } from '../src'; +import { CatalogEntityPage } from '@backstage/plugin-catalog'; type DataRelation = [string, string, string]; type DataEntity = [string, string, DataRelation[]]; @@ -117,7 +118,12 @@ const entities = ( name, }, relations: relations.map(([type, k, n]) => ({ - target: { kind: k, name: n, namespace: ENTITY_DEFAULT_NAMESPACE }, + target: { kind: k, name: n, namespace: DEFAULT_NAMESPACE }, + targetRef: stringifyEntityRef({ + kind: k, + namespace: DEFAULT_NAMESPACE, + name: n, + }), type, })), }; @@ -136,7 +142,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; @@ -162,6 +168,12 @@ createDevApp() ), }) .addPage({ + path: '/catalog-graph', element: , }) + .addPage({ + path: '/catalog/:kind/:namespace/:name', + element: , + title: 'MockComponent', + }) .render(); diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 541d21f319..0d85123646 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.10", + "version": "0.2.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,23 +10,26 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.6.0", - "@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/theme": "^0.2.14", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -42,10 +45,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/plugin-catalog": "^0.9.0", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@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/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 7209a516a5..7cebdee692 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -60,12 +60,12 @@ describe('', () => { getEntityByName: jest.fn(async _ => ({ ...entity, relations: [] })), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; apis = TestApiRegistry.from([catalogApiRef, catalog]); diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index b2dfd9f9b2..e6811aad62 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -21,7 +21,7 @@ import { import { InfoCard, InfoCardVariants } from '@backstage/core-components'; import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api'; import { - formatEntityRefTitle, + humanizeEntityRef, useEntity, entityRouteRef, } from '@backstage/plugin-catalog-react'; @@ -94,7 +94,7 @@ export const CatalogGraphCard = ({ }); analytics.captureEvent( 'click', - node.title ?? formatEntityRefTitle(nodeEntityName), + node.title ?? humanizeEntityRef(nodeEntityName), { attributes: { to: path } }, ); navigate(path); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index 4c7a64a7ce..2f686f8f62 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -58,6 +58,7 @@ describe('', () => { relations: [ { type: RELATION_PART_OF, + targetRef: 'b:d/e', target: { kind: 'b', namespace: 'd', @@ -76,6 +77,7 @@ describe('', () => { relations: [ { type: RELATION_HAS_PART, + targetRef: 'b:d/c', target: { kind: 'b', namespace: 'd', @@ -89,12 +91,12 @@ describe('', () => { getEntityByName: jest.fn(async n => (n.name === 'e' ? entityE : entityC)), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; wrapper = ( diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index 2ae7a99dd8..15d0cd139b 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -24,7 +24,7 @@ import { import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api'; import { entityRouteRef, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { Grid, makeStyles, Paper, Typography } from '@material-ui/core'; import FilterListIcon from '@material-ui/icons/FilterList'; @@ -149,14 +149,14 @@ export const CatalogGraphPage = ({ analytics.captureEvent( 'click', - node.title ?? formatEntityRefTitle(nodeEntityName), + node.title ?? humanizeEntityRef(nodeEntityName), { attributes: { to: path } }, ); navigate(path); } else { analytics.captureEvent( 'click', - node.title ?? formatEntityRefTitle(nodeEntityName), + node.title ?? humanizeEntityRef(nodeEntityName), ); setRootEntityNames([nodeEntityName]); } @@ -168,7 +168,7 @@ export const CatalogGraphPage = ({
formatEntityRefTitle(e)).join(', ')} + subtitle={rootEntityNames.map(e => humanizeEntityRef(e)).join(', ')} /> >; -const useEntityKinds = useEntityKindsMocked as jest.Mock< - ReturnType ->; +const catalogApi = { + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { + kind: [ + { value: 'Component', count: 2 }, + { value: 'System', count: 1 }, + { value: 'API', count: 1 }, + { value: 'Resource', count: 1 }, + ], + }, + } as GetEntityFacetsResponse), +}; +const apis = TestApiRegistry.from( + [catalogApiRef, catalogApi], + [alertApiRef, {} as AlertApi], +); describe('', () => { - beforeEach(() => { - useApi.mockReturnValue({}); - useEntityKinds.mockReturnValue({ - loading: false, - kinds: ['API', 'Component', 'System', 'Domain', 'Resource'], - error: undefined, - }); + it('should not explode while loading', async () => { + const rendered = await renderWithEffects( + + {}} /> + , + ); + expect(rendered.baseElement).toBeInTheDocument(); }); - afterEach(() => jest.resetAllMocks()); - - test('should not explode while loading', () => { - useEntityKinds.mockReturnValue({ - loading: true, - kinds: undefined, - error: undefined, - }); - const { baseElement } = render( - {}} />, + it('should render current value', async () => { + const rendered = await renderWithEffects( + + {}} /> + , ); - expect(baseElement).toBeInTheDocument(); + expect(rendered.getByText('API')).toBeInTheDocument(); + expect(rendered.getByText('Component')).toBeInTheDocument(); }); - test('should render current value', () => { - const { getByText } = render( - {}} />, - ); - - expect(getByText('API')).toBeInTheDocument(); - expect(getByText('Component')).toBeInTheDocument(); - }); - - test('should select value', async () => { + it('should select value', async () => { const onChange = jest.fn(); - const { getByText, getByLabelText } = render( - , + const { getByLabelText, getByText } = await renderWithEffects( + + + , ); userEvent.click(getByLabelText('Open')); - await waitFor(() => expect(getByText('System')).toBeInTheDocument()); userEvent.click(getByText('System')); @@ -79,15 +79,16 @@ describe('', () => { }); }); - test('should return undefined if all values are selected', async () => { + it('should return undefined if all values are selected', async () => { const onChange = jest.fn(); - const { getByText, getByLabelText } = render( - , + const { getByLabelText, getByText } = await renderWithEffects( + + + , ); - userEvent.click(getByLabelText('Open')); await waitFor(() => expect(getByText('Resource')).toBeInTheDocument()); @@ -99,10 +100,12 @@ describe('', () => { }); }); - test('should return all values when cleared', async () => { + it('should return all values when cleared', async () => { const onChange = jest.fn(); - const { getByRole } = render( - , + const { getByRole } = await renderWithEffects( + + + , ); userEvent.click(getByRole('combobox')); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx index 87ba8e77bf..316f83da62 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { alertApiRef, useApi } from '@backstage/core-plugin-api'; -import { useEntityKinds } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Box, Checkbox, @@ -28,6 +28,7 @@ import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useCallback, useEffect, useMemo } from 'react'; +import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles({ formControl: { @@ -43,7 +44,13 @@ export type Props = { export const SelectedKindsFilter = ({ value, onChange }: Props) => { const classes = useStyles(); const alertApi = useApi(alertApiRef); - const { error, kinds } = useEntityKinds(); + const catalogApi = useApi(catalogApiRef); + + const { error, value: kinds } = useAsync(async () => { + return await catalogApi + .getEntityFacets({ facets: ['kind'] }) + .then(response => response.facets.kind?.map(f => f.value).sort() || []); + }); useEffect(() => { if (error) { diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx index 9cd752a5d7..ae541fa997 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { DependencyGraphTypes } from '@backstage/core-components'; -import { formatEntityRefTitle } from '@backstage/plugin-catalog-react'; +import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; import { BackstageTheme } from '@backstage/theme'; import { makeStyles } from '@material-ui/core/styles'; import classNames from 'classnames'; @@ -95,7 +95,7 @@ export function CustomNode({ const displayTitle = title ?? (kind && name && namespace - ? formatEntityRefTitle({ kind, name, namespace }) + ? humanizeEntityRef({ kind, name, namespace }) : id); return ( diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx index be0c4dc5e5..36dc816444 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx @@ -54,6 +54,7 @@ describe('', () => { name: 'a1', namespace: 'd', }, + targetRef: 'k:d/a1', type: RELATION_OWNER_OF, }, { @@ -62,6 +63,7 @@ describe('', () => { name: 'c1', namespace: 'd', }, + targetRef: 'b:d/c1', type: RELATION_HAS_PART, }, ], @@ -80,6 +82,7 @@ describe('', () => { name: 'c', namespace: 'd', }, + targetRef: 'b:d/c', type: RELATION_OWNED_BY, }, { @@ -88,6 +91,7 @@ describe('', () => { name: 'c1', namespace: 'd', }, + targetRef: 'b:d/c1', type: RELATION_OWNED_BY, }, ], @@ -106,6 +110,7 @@ describe('', () => { name: 'c', namespace: 'd', }, + targetRef: 'b:d/c', type: RELATION_PART_OF, }, { @@ -114,6 +119,7 @@ describe('', () => { name: 'a1', namespace: 'd', }, + targetRef: 'k:d/a1', type: RELATION_OWNER_OF, }, { @@ -122,6 +128,7 @@ describe('', () => { name: 'c2', namespace: 'd', }, + targetRef: 'b:d/c2', type: RELATION_HAS_PART, }, ], @@ -140,6 +147,7 @@ describe('', () => { name: 'c1', namespace: 'd', }, + targetRef: 'b:d/c1', type: RELATION_PART_OF, }, ], @@ -150,12 +158,12 @@ describe('', () => { getEntityByName: jest.fn(async n => entities[stringifyEntityRef(n)]), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; Wrapper = ({ children }) => ( @@ -225,6 +233,7 @@ describe('', () => { name: 'some-component', namespace: 'default', }, + targetRef: 'component:default/some-component', type: RELATION_OWNER_OF, }, ], diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts index bf6fc9ee3f..7e0cd411ce 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts @@ -49,6 +49,7 @@ describe('useEntityRelationGraph', () => { name: 'a1', namespace: 'd', }, + targetRef: 'k:d/a1', type: RELATION_OWNER_OF, }, { @@ -57,6 +58,7 @@ describe('useEntityRelationGraph', () => { name: 'c1', namespace: 'd', }, + targetRef: 'b:d/c1', type: RELATION_HAS_PART, }, ], @@ -75,6 +77,7 @@ describe('useEntityRelationGraph', () => { name: 'c', namespace: 'd', }, + targetRef: 'b:d/c', type: RELATION_OWNED_BY, }, { @@ -83,6 +86,7 @@ describe('useEntityRelationGraph', () => { name: 'c1', namespace: 'd', }, + targetRef: 'b:d/c1', type: RELATION_OWNED_BY, }, ], @@ -101,6 +105,7 @@ describe('useEntityRelationGraph', () => { name: 'c', namespace: 'd', }, + targetRef: 'b:d/c', type: RELATION_PART_OF, }, { @@ -109,6 +114,7 @@ describe('useEntityRelationGraph', () => { name: 'a1', namespace: 'd', }, + targetRef: 'k:d/a1', type: RELATION_OWNER_OF, }, { @@ -117,6 +123,7 @@ describe('useEntityRelationGraph', () => { name: 'c2', namespace: 'd', }, + targetRef: 'b:d/c2', type: RELATION_HAS_PART, }, ], @@ -135,6 +142,7 @@ describe('useEntityRelationGraph', () => { name: 'c1', namespace: 'd', }, + targetRef: 'b:d/c1', type: RELATION_PART_OF, }, ], diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts index e0d393168b..957f35dc25 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { useEffect } from 'react'; import { useEntityStore } from './useEntityStore'; @@ -64,13 +64,15 @@ export function useEntityRelationGraph({ if ( (!relations || relations.includes(rel.type)) && (!kinds || - kinds.includes(rel.target.kind.toLocaleLowerCase('en-US'))) + kinds.some(kind => + rel.targetRef.startsWith( + `${kind.toLocaleLowerCase('en-US')}:`, + ), + )) ) { - const relationEntityRef = stringifyEntityRef(rel.target); - - if (!processedEntityRefs.has(relationEntityRef)) { - nextDepthRefQueue.push(relationEntityRef); - expectedEntities.add(relationEntityRef); + if (!processedEntityRefs.has(rel.targetRef)) { + nextDepthRefQueue.push(rel.targetRef); + expectedEntities.add(rel.targetRef); } } } diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts index 37073352d6..4715cb106a 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts @@ -49,6 +49,7 @@ describe('useEntityRelationNodesAndEdges', () => { name: 'a1', namespace: 'd', }, + targetRef: 'k:d/a1', type: RELATION_OWNER_OF, }, { @@ -57,6 +58,7 @@ describe('useEntityRelationNodesAndEdges', () => { name: 'c1', namespace: 'd', }, + targetRef: 'b:d/c1', type: RELATION_HAS_PART, }, ], @@ -75,6 +77,7 @@ describe('useEntityRelationNodesAndEdges', () => { name: 'c', namespace: 'd', }, + targetRef: 'b:d/c', type: RELATION_OWNED_BY, }, { @@ -83,6 +86,7 @@ describe('useEntityRelationNodesAndEdges', () => { name: 'c1', namespace: 'd', }, + targetRef: 'b:d/c1', type: RELATION_OWNED_BY, }, ], @@ -101,6 +105,7 @@ describe('useEntityRelationNodesAndEdges', () => { name: 'c', namespace: 'd', }, + targetRef: 'b:d/c', type: RELATION_PART_OF, }, { @@ -109,6 +114,7 @@ describe('useEntityRelationNodesAndEdges', () => { name: 'a1', namespace: 'd', }, + targetRef: 'k:d/a1', type: RELATION_OWNER_OF, }, { @@ -117,6 +123,7 @@ describe('useEntityRelationNodesAndEdges', () => { name: 'c2', namespace: 'd', }, + targetRef: 'b:d/c2', type: RELATION_HAS_PART, }, ], @@ -135,6 +142,7 @@ describe('useEntityRelationNodesAndEdges', () => { name: 'c1', namespace: 'd', }, + targetRef: 'b:d/c1', type: RELATION_PART_OF, }, ], diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts index 2f78fd9c17..a1c3416d4f 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - ENTITY_DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; +import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { MouseEvent, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { RelationPairs, ALL_RELATION_PAIRS } from './relations'; @@ -77,7 +74,7 @@ export function useEntityRelationNodesAndEdges({ title: entity.metadata?.title ?? undefined, kind: entity.kind, name: entity.metadata.name, - namespace: entity.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE, + namespace: entity.metadata.namespace ?? DEFAULT_NAMESPACE, focused, color: focused ? 'secondary' : 'primary', }; @@ -100,11 +97,9 @@ export function useEntityRelationNodesAndEdges({ if (entity) { entity?.relations?.forEach(rel => { - const targetRef = stringifyEntityRef(rel.target); - // Check if the related entity should be displayed, if not, ignore // the relation too - if (!entities[targetRef]) { + if (!entities[rel.targetRef]) { return; } @@ -114,12 +109,14 @@ export function useEntityRelationNodesAndEdges({ if ( kinds && - !kinds.includes(rel.target.kind.toLocaleLowerCase('en-US')) + !kinds.some(kind => + rel.targetRef.startsWith(`${kind.toLocaleLowerCase('en-US')}:`), + ) ) { return; } - if (!unidirectional || !visitedNodes.has(targetRef)) { + if (!unidirectional || !visitedNodes.has(rel.targetRef)) { if (mergeRelations) { const pair = relationPairs.find( ([l, r]) => l === rel.type || r === rel.type, @@ -127,24 +124,24 @@ export function useEntityRelationNodesAndEdges({ const [left] = pair; edges.push({ - from: left === rel.type ? entityRef : targetRef, - to: left === rel.type ? targetRef : entityRef, + from: left === rel.type ? entityRef : rel.targetRef, + to: left === rel.type ? rel.targetRef : entityRef, relations: pair, label: 'visible', }); } else { edges.push({ from: entityRef, - to: targetRef, + to: rel.targetRef, relations: [rel.type], label: 'visible', }); } } - if (!visitedNodes.has(targetRef)) { - nodeQueue.push(targetRef); - visitedNodes.add(targetRef); + if (!visitedNodes.has(rel.targetRef)) { + nodeQueue.push(rel.targetRef); + visitedNodes.add(rel.targetRef); } }); } diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts index a60750bab5..a5dc82eab9 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts @@ -32,12 +32,12 @@ describe('useEntityStore', () => { getEntityByName: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; useApi.mockReturnValue(catalogApi); diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index f2ad5d14b4..cd1f255e23 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-catalog-graphql +## 0.3.4 + +### Patch Changes + +- 68913af4ff: chore(deps): bump `graphql-modules` from 1.4.4 to 2.0.0 +- Updated dependencies + - @backstage/catalog-model@0.11.0 + +## 0.3.3 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/types@0.1.3 + +## 0.3.2 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.3.1 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index b757790275..6ecf7eb512 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-graphql", "description": "An experimental Backstage catalog GraphQL module", - "version": "0.3.1", + "version": "0.3.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,20 +24,20 @@ "graphql" ], "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", + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", "generate:types": "graphql-codegen", - "clean": "backstage-cli clean" + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/types": "^0.1.1", - "graphql-modules": "^1.0.0", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/types": "^0.1.3", + "graphql-modules": "^2.0.0", "apollo-server": "^3.0.0", "graphql": "^16.0.0", "graphql-tag": "^2.11.0", @@ -43,8 +46,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", - "@backstage/test-utils": "^0.2.4-next.0", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@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 8c86fd4ca2..b55f8b34fa 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-catalog-import +## 0.8.3 + +### Patch Changes + +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration@0.7.5 + - @backstage/integration-react@0.1.23 + +## 0.8.2 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- Updated dependencies + - @backstage/catalog-client@0.7.0 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/integration-react@0.1.22 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + ## 0.8.1 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 7ce6f8524f..800b747e8a 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.8.1", + "version": "0.8.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,25 +24,25 @@ "backstage" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.6.0", - "@backstage/catalog-model": "^0.9.10", - "@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.21", - "@backstage/plugin-catalog-react": "^0.6.14", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/config": "^0.1.15", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/integration-react": "^0.1.23", + "@backstage/plugin-catalog-react": "^0.7.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -57,10 +60,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@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-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index fa63825af1..f689c0a883 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -94,12 +94,12 @@ describe('CatalogImportClient', () => { addLocation: jest.fn(), removeLocationById: jest.fn(), getEntityByName: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; let catalogImportClient: CatalogImportClient; diff --git a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx index 2c59eec18f..ceec4b97e8 100644 --- a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx +++ b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx @@ -18,7 +18,7 @@ import { Entity, EntityName } from '@backstage/catalog-model'; import { useApp } from '@backstage/core-plugin-api'; import { EntityRefLink, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { Collapse, @@ -43,7 +43,7 @@ const useStyles = makeStyles(theme => ({ function sortEntities(entities: Array) { return entities.sort((a, b) => - formatEntityRefTitle(a).localeCompare(formatEntityRefTitle(b)), + humanizeEntityRef(a).localeCompare(humanizeEntityRef(b)), ); } @@ -130,7 +130,7 @@ export const EntityListComponent = (props: EntityListComponentProps) => { ) ?? WorkIcon; return ( { - + ); })} diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 8d26657bb3..fc9b35136a 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -39,13 +39,13 @@ describe('', () => { getEntities: jest.fn(), addLocation: jest.fn(), getEntityByName: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; const errorApi: jest.Mocked = { diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index 36e8c33a5c..1cbdfce355 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -19,7 +19,7 @@ import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; import { catalogApiRef, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { Box, FormHelperText, Grid, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; @@ -139,7 +139,7 @@ export const StepPrepareCreatePullRequest = ( }); return groupEntities.items - .map(e => formatEntityRefTitle(e, { defaultKind: 'group' })) + .map(e => humanizeEntityRef(e, { defaultKind: 'group' })) .sort(); }); diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 65fd275389..ac611032e5 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-catalog-react +## 0.7.0 + +### Minor Changes + +- 3334ad47d4: Removed the deprecated `EntityContext` which have been replaced by `useEntity`, `EntityProvider` and `AsyncEntityProvider`. +- e2e0b6625c: Improved API documentation. + + **BREAKING**: The individual table column factories (e.g. `createEntityRefColumn`) are now no longer available directly, but only through `EntityTable.columns`. + +- c4276915c0: **BREAKING**: Deleted the deprecated `loadIdentityOwnerRefs` function which is replaced by `ownershipEntityRefs` from `identityApi.getBackstageIdentity()`. + + Deprecated the `loadCatalogOwnerRefs` hook as membership references should be added as `ent` inside `claims` sections of the `SignInResolver` when issuing tokens. See https://backstage.io/docs/auth/identity-resolver for more details on how to prepare your `SignInResolver` if not done already. Usage of the `loadCatalogOwnerRefs` hook should be replaced by `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. + +### Patch Changes + +- a8331830f1: Deprecated the `useEntityKinds` hook due to low usage and utility value. +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- b776ce5aab: Deprecated the `useEntityListProvider` hook which is now renamed to `useEntityList` +- b3ef24038b: Deprecated `reduceCatalogFilters` and `reduceEntityFilters` as these helper functions are used internally and provides low external value. +- 2d339b5f2c: Deprecated `useEntityFromUrl` and the `useEntityCompoundName` hooks as these have very low utility value. +- 96b8ae9a9e: Deprecated the `EntityTypeReturn` type and inlined the return type to `useEntityTypeFilter` as the type and function name does not align +- d4f67fa728: Deprecated the `useOwnedEntities` hook which is replaced by the IdentityAPI. + Deprecated the `useOwnUser` hook due to low external value. +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration@0.7.5 + - @backstage/plugin-permission-react@0.3.2 + +## 0.6.15 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 538ca90790: Use updated type names from `@backstage/catalog-client` +- edbc03814a: Replace usage of `serializeEntityRef` with `stringifyEntityRef` +- 244d24ebc4: Import `Location` from the `@backstage/catalog-client` package. +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- 216725b434: Updated to use new names for `parseLocationRef` and `stringifyLocationRef` +- e72d371296: Use `TemplateEntityV1beta2` from `@backstage/plugin-scaffolder-common` instead + of `@backstage/catalog-model`. +- 98d1aa1ea1: Fix CatalogPage showing all components when owned filter was pre-selected +- 27eccab216: Replaces use of deprecated catalog-model constants. +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/catalog-client@0.7.0 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/plugin-permission-common@0.5.0 + - @backstage/plugin-permission-react@0.3.1 + - @backstage/catalog-model@0.10.0 + - @backstage/types@0.1.2 + - @backstage/version-bridge@0.1.2 + ## 0.6.14 ### Patch Changes diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 056f10f135..394a45f278 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -9,14 +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'; import { Observable } from '@backstage/types'; import { Overrides } from '@material-ui/core/styles/overrides'; @@ -26,7 +24,6 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { StorageApi } from '@backstage/core-plugin-api'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { SystemEntity } from '@backstage/catalog-model'; import { TableColumn } from '@backstage/core-components'; @@ -66,9 +63,7 @@ export { CATALOG_FILTER_EXISTS }; export { CatalogApi }; -// Warning: (ae-missing-release-tag) "catalogApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const catalogApiRef: ApiRef; // @public (undocumented) @@ -100,51 +95,34 @@ export type CatalogReactUserListPickerClassKey = | 'menuItem' | 'groupWrapper'; -// Warning: (ae-missing-release-tag) "catalogRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const catalogRouteRef: RouteRef; - // @public (undocumented) -function createDomainColumn(): TableColumn; +export const columnFactories: Readonly<{ + createEntityRefColumn(options: { + defaultKind?: string; + }): TableColumn; + createEntityRelationColumn({ + title, + relation, + defaultKind, + filter: entityFilter, + }: { + title: string; + relation: string; + defaultKind?: string | undefined; + filter?: + | { + kind: string; + } + | undefined; + }): TableColumn; + createOwnerColumn(): TableColumn; + createDomainColumn(): TableColumn; + createSystemColumn(): TableColumn; + createMetadataDescriptionColumn(): TableColumn; + createSpecLifecycleColumn(): TableColumn; + createSpecTypeColumn(): TableColumn; +}>; -// @public (undocumented) -function createEntityRefColumn(options: { - defaultKind?: string; -}): TableColumn; - -// @public (undocumented) -function createEntityRelationColumn({ - title, - relation, - defaultKind, - filter: entityFilter, -}: { - title: string; - relation: string; - defaultKind?: string; - filter?: { - kind: string; - }; -}): TableColumn; - -// @public (undocumented) -function createMetadataDescriptionColumn(): TableColumn; - -// @public (undocumented) -function createOwnerColumn(): TableColumn; - -// @public (undocumented) -function createSpecLifecycleColumn(): TableColumn; - -// @public (undocumented) -function createSpecTypeColumn(): TableColumn; - -// @public (undocumented) -function createSystemColumn(): TableColumn; - -// Warning: (ae-missing-release-tag) "DefaultEntityFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type DefaultEntityFilters = { kind?: EntityKindFilter; @@ -156,24 +134,6 @@ export type DefaultEntityFilters = { text?: EntityTextFilter; }; -// @public -export class DefaultStarredEntitiesApi implements StarredEntitiesApi { - constructor(opts: { storageApi: StorageApi }); - // (undocumented) - isStarred(entityRef: string): boolean; - // (undocumented) - starredEntitie$(): Observable>; - // (undocumented) - toggleStarred(entityRef: string): Promise; -} - -// Warning: (ae-forgotten-export) The symbol "EntityLoadingStatus" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export const EntityContext: Context; - -// Warning: (ae-missing-release-tag) "EntityFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type EntityFilter = { getCatalogFilters?: () => Record< @@ -184,9 +144,7 @@ export type EntityFilter = { toQueryValue?: () => string | string[]; }; -// Warning: (ae-missing-release-tag) "EntityKindFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class EntityKindFilter implements EntityFilter { constructor(value: string); // (undocumented) @@ -210,9 +168,7 @@ export interface EntityKindPickerProps { 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) -// -// @public (undocumented) +// @public export class EntityLifecycleFilter implements EntityFilter { constructor(values: string[]); // (undocumented) @@ -226,24 +182,42 @@ export class EntityLifecycleFilter implements EntityFilter { // @public (undocumented) export const EntityLifecyclePicker: () => JSX.Element | null; -// Warning: (ae-forgotten-export) The symbol "EntityListContextProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityListContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const EntityListContext: React_2.Context< EntityListContextProps | undefined >; -// Warning: (ae-missing-release-tag) "EntityListProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) +export type EntityListContextProps< + EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, +> = { + filters: EntityFilters; + entities: Entity[]; + backendEntities: Entity[]; + updateFilters: ( + filters: + | Partial + | ((prevFilters: EntityFilters) => Partial), + ) => void; + queryParameters: Partial>; + loading: boolean; + error?: Error; +}; + +// @public export const EntityListProvider: ({ children, }: PropsWithChildren<{}>) => JSX.Element; -// Warning: (ae-missing-release-tag) "EntityOwnerFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) +export type EntityLoadingStatus = { + entity?: TEntity; + loading: boolean; + error?: Error; + refresh?: VoidFunction; +}; + +// @public export class EntityOwnerFilter implements EntityFilter { constructor(values: string[]); // (undocumented) @@ -276,7 +250,7 @@ export const EntityRefLink: (props: EntityRefLinkProps) => JSX.Element; // @public export type EntityRefLinkProps = { - entityRef: Entity | EntityName; + entityRef: Entity | EntityName | string; defaultKind?: string; title?: string; children?: React_2.ReactNode; @@ -295,26 +269,13 @@ export type EntityRefLinksProps = { 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) -export const entityRoute: RouteRef<{ - name: string; - kind: string; - namespace: string; -}>; - -// Warning: (ae-missing-release-tag) "entityRouteParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function entityRouteParams(entity: Entity): { readonly kind: string; readonly namespace: string; readonly name: string; }; -// Warning: (ae-missing-release-tag) "entityRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const entityRouteRef: RouteRef<{ name: string; @@ -322,38 +283,48 @@ export const entityRouteRef: RouteRef<{ namespace: string; }>; -// @public (undocumented) +// @public export const EntitySearchBar: () => JSX.Element; -// Warning: (ae-missing-release-tag) "EntitySourceLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type EntitySourceLocation = { locationTargetUrl: string; integrationType?: string; }; -// Warning: (ae-missing-release-tag) "EntityTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export function EntityTable( - props: EntityTableProps, -): JSX.Element; +export const EntityTable: { + (props: EntityTableProps): JSX.Element; + columns: Readonly<{ + createEntityRefColumn(options: { + defaultKind?: string | undefined; + }): TableColumn; + createEntityRelationColumn({ + title, + relation, + defaultKind, + filter: entityFilter, + }: { + title: string; + relation: string; + defaultKind?: string | undefined; + filter?: + | { + kind: string; + } + | undefined; + }): TableColumn; + createOwnerColumn(): TableColumn; + createDomainColumn(): TableColumn; + createSystemColumn(): TableColumn; + createMetadataDescriptionColumn(): TableColumn; + createSpecLifecycleColumn(): TableColumn; + createSpecTypeColumn(): TableColumn; + }>; + systemEntityColumns: TableColumn[]; + componentEntityColumns: TableColumn[]; +}; -// @public (undocumented) -export namespace EntityTable { - var // Warning: (ae-forgotten-export) The symbol "columnFactories" needs to be exported by the entry point index.d.ts - // - // (undocumented) - columns: typeof columnFactories; - var // (undocumented) - systemEntityColumns: TableColumn[]; - var // (undocumented) - 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) @@ -368,9 +339,7 @@ export interface EntityTableProps { 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) +// @public export class EntityTagFilter implements EntityFilter { constructor(values: string[]); // (undocumented) @@ -384,9 +353,7 @@ export class EntityTagFilter implements EntityFilter { // @public (undocumented) export const EntityTagPicker: () => JSX.Element | null; -// Warning: (ae-missing-release-tag) "EntityTextFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class EntityTextFilter implements EntityFilter { constructor(value: string); // (undocumented) @@ -395,9 +362,7 @@ export class EntityTextFilter implements EntityFilter { readonly value: string; } -// Warning: (ae-missing-release-tag) "EntityTypeFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class EntityTypeFilter implements EntityFilter { constructor(value: string | string[]); // (undocumented) @@ -423,46 +388,40 @@ export interface EntityTypePickerProps { 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) => JSX.Element; +// @public @deprecated (undocumented) +export type EntityTypeReturn = { + loading: boolean; + error?: Error; + availableTypes: string[]; + selectedTypes: string[]; + setSelectedTypes: (types: string[]) => void; +}; + +// @public +export const FavoriteEntity: (props: FavoriteEntityProps) => 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) -// // @public (undocumented) export const favoriteEntityIcon: (isStarred: boolean) => JSX.Element; -// Warning: (ae-missing-release-tag) "favoriteEntityTooltip" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export type FavoriteEntityProps = ComponentProps & { + entity: Entity; +}; + // @public (undocumented) export const favoriteEntityTooltip: ( isStarred: boolean, ) => 'Remove from favorites' | 'Add to favorites'; -// Warning: (ae-missing-release-tag) "formatEntityRefTitle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function formatEntityRefTitle( - entityRef: Entity | EntityName, - opts?: { - defaultKind?: string; - }, -): string; +// @public @deprecated (undocumented) +export const formatEntityRefTitle: typeof humanizeEntityRef; -// Warning: (ae-missing-release-tag) "getEntityMetadataEditUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export function getEntityMetadataEditUrl(entity: Entity): string | undefined; -// Warning: (ae-missing-release-tag) "getEntityMetadataViewUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export function getEntityMetadataViewUrl(entity: Entity): string | undefined; -// Warning: (ae-missing-release-tag) "getEntityRelations" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getEntityRelations( entity: Entity | undefined, @@ -472,14 +431,20 @@ export function getEntityRelations( }, ): EntityName[]; -// Warning: (ae-missing-release-tag) "getEntitySourceLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function getEntitySourceLocation( entity: Entity, scmIntegrationsApi: ScmIntegrationRegistry, ): EntitySourceLocation | undefined; +// @public (undocumented) +export function humanizeEntityRef( + entityRef: Entity | EntityName, + opts?: { + defaultKind?: string; + }, +): string; + // @public export function InspectEntityDialog(props: { open: boolean; @@ -487,24 +452,15 @@ export function InspectEntityDialog(props: { 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 export function isOwnerOf(owner: Entity, owned: Entity): boolean; -// @public +// @public @deprecated export function loadCatalogOwnerRefs( catalogApi: CatalogApi, identityOwnerRefs: string[], ): Promise; -// @public @deprecated -export function loadIdentityOwnerRefs( - identityApi: IdentityApi, -): Promise; - -// Warning: (ae-missing-release-tag) "MockEntityListContextProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const MockEntityListContextProvider: ({ children, @@ -513,25 +469,24 @@ export const MockEntityListContextProvider: ({ value?: Partial> | undefined; }>) => JSX.Element; -// Warning: (ae-missing-release-tag) "reduceCatalogFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export class MockStarredEntitiesApi implements StarredEntitiesApi { + // (undocumented) + starredEntitie$(): Observable>; + // (undocumented) + toggleStarred(entityRef: string): Promise; +} + +// @public @deprecated (undocumented) export function reduceCatalogFilters( filters: EntityFilter[], ): Record; -// Warning: (ae-missing-release-tag) "reduceEntityFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export function reduceEntityFilters( filters: EntityFilter[], ): (entity: Entity) => boolean; -// Warning: (ae-missing-release-tag) "rootRoute" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const rootRoute: RouteRef; - // @public export interface StarredEntitiesApi { starredEntitie$(): Observable>; @@ -541,51 +496,55 @@ export interface StarredEntitiesApi { // @public export const starredEntitiesApiRef: ApiRef; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "UnregisterEntityDialog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const UnregisterEntityDialog: ({ - open, - onConfirm, - onClose, - entity, -}: Props_2) => JSX.Element; +export const UnregisterEntityDialog: ( + props: UnregisterEntityDialogProps, +) => JSX.Element; -// @public -export function useEntity(): { - entity: T; - loading: boolean; - error: Error | undefined; - refresh: VoidFunction | undefined; +// @public (undocumented) +export type UnregisterEntityDialogProps = { + open: boolean; + onConfirm: () => any; + onClose: () => any; + entity: Entity; }; -// Warning: (ae-missing-release-tag) "useEntityCompoundName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public +export function useAsyncEntity< + TEntity extends Entity = Entity, +>(): EntityLoadingStatus; + +// @public +export function useEntity(): { + entity: TEntity; + loading: boolean; + error?: Error; + refresh?: VoidFunction; +}; + +// @public @deprecated export const useEntityCompoundName: () => { kind: string; namespace: string; name: string; }; -// Warning: (ae-missing-release-tag) "useEntityFromUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const useEntityFromUrl: () => EntityLoadingStatus; -// Warning: (ae-missing-release-tag) "useEntityKinds" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated export function useEntityKinds(): { error: Error | undefined; loading: boolean; kinds: string[] | undefined; }; -// Warning: (ae-missing-release-tag) "useEntityListProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export function useEntityList< + EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, +>(): EntityListContextProps; + +// @public @deprecated export function useEntityListProvider< EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, >(): EntityListContextProps; @@ -593,42 +552,38 @@ export function useEntityListProvider< // @public export function useEntityOwnership(): { loading: boolean; - isOwnedEntity: (entity: Entity | EntityName) => boolean; + isOwnedEntity: (entity: Entity) => boolean; }; -// @public +// @alpha export function useEntityPermission(permission: Permission): { loading: boolean; allowed: boolean; error?: Error; }; -// Warning: (ae-forgotten-export) The symbol "EntityTypeReturn" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "useEntityTypeFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export function useEntityTypeFilter(): EntityTypeReturn; - -// @public -export function useOwnedEntities(allowedKinds?: string[]): { +export function useEntityTypeFilter(): { loading: boolean; - ownedEntities: CatalogListResponse | undefined; + error?: Error; + availableTypes: string[]; + selectedTypes: string[]; + setSelectedTypes: (types: string[]) => void; }; -// Warning: (ae-missing-release-tag) "useOwnUser" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @public @deprecated +export function useOwnedEntities(allowedKinds?: string[]): { + loading: boolean; + ownedEntities: GetEntitiesResponse | undefined; +}; + +// @public @deprecated export function useOwnUser(): AsyncState; -// Warning: (ae-missing-release-tag) "useRelatedEntities" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function useRelatedEntities( entity: Entity, - { - type, - kind, - }: { + relationFilter: { type?: string; kind?: string; }, @@ -638,9 +593,7 @@ export function useRelatedEntities( error: Error | undefined; }; -// Warning: (ae-missing-release-tag) "UserListFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class UserListFilter implements EntityFilter { constructor( value: UserListFilterKind, @@ -659,22 +612,18 @@ export class UserListFilter implements EntityFilter { readonly value: UserListFilterKind; } -// Warning: (ae-missing-release-tag) "UserListFilterKind" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type UserListFilterKind = 'owned' | 'starred' | 'all'; -// Warning: (ae-forgotten-export) The symbol "UserListPickerProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "UserListPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const UserListPicker: ({ - initialFilter, - availableFilters, -}: UserListPickerProps) => JSX.Element; +export const UserListPicker: (props: UserListPickerProps) => JSX.Element; + +// @public (undocumented) +export type UserListPickerProps = { + initialFilter?: UserListFilterKind; + availableFilters?: UserListFilterKind[]; +}; -// Warning: (ae-missing-release-tag) "useStarredEntities" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function useStarredEntities(): { starredEntities: Set; @@ -682,8 +631,6 @@ export function useStarredEntities(): { isStarredEntity: (entityOrRef: Entity | EntityName | string) => boolean; }; -// Warning: (ae-missing-release-tag) "useStarredEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function useStarredEntity(entityOrRef: Entity | EntityName | string): { toggleStarredEntity: () => void; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index d995fdf848..119c3f19a4 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.14", + "version": "0.7.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,7 +9,11 @@ "publishConfig": { "access": "public", "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" + }, + "backstage": { + "role": "web-library" }, "homepage": "https://backstage.io", "repository": { @@ -21,24 +25,25 @@ "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" + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-client": "^0.6.0", - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/integration": "^0.7.2", - "@backstage/plugin-permission-common": "^0.4.0", - "@backstage/plugin-permission-react": "^0.3.0", - "@backstage/types": "^0.1.1", - "@backstage/version-bridge": "^0.1.1", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/types": "^0.1.3", + "@backstage/version-bridge": "^0.1.2", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -56,10 +61,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@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", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/plugin-catalog-common": "^0.1.4", + "@backstage/plugin-scaffolder-common": "^0.2.2", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -71,6 +77,7 @@ "react-test-renderer": "^16.13.1" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/catalog-react/src/api.ts b/plugins/catalog-react/src/api.ts index 51146b95bf..38e1f618cd 100644 --- a/plugins/catalog-react/src/api.ts +++ b/plugins/catalog-react/src/api.ts @@ -17,6 +17,10 @@ import { CatalogApi } from '@backstage/catalog-client'; import { createApiRef } from '@backstage/core-plugin-api'; +/** + * The API reference for the {@link @backstage/catalog-client#CatalogApi}. + * @public + */ export const catalogApiRef = createApiRef({ id: 'plugin.catalog.service', }); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts deleted file mode 100644 index 55b992fadf..0000000000 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { stringifyEntityRef } from '@backstage/catalog-model'; -import { StorageApi } from '@backstage/core-plugin-api'; -import { MockStorageApi } from '@backstage/test-utils'; -import { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; -import { performMigrationToTheNewBucket } from './migration'; - -jest.mock('./migration'); - -describe('DefaultStarredEntitiesApi', () => { - let mockStorage: StorageApi; - let starredEntitiesApi: DefaultStarredEntitiesApi; - - const mockEntityRef = stringifyEntityRef({ - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'mock', - }, - }); - - beforeEach(() => { - (performMigrationToTheNewBucket as jest.Mock).mockResolvedValue(undefined); - - mockStorage = MockStorageApi.create(); - starredEntitiesApi = new DefaultStarredEntitiesApi({ - storageApi: mockStorage, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('constructor', () => { - it('should call migration', () => { - expect(performMigrationToTheNewBucket).toBeCalledTimes(1); - }); - }); - - describe('toggleStarred', () => { - it('should star unstarred entity', async () => { - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(false); - - await starredEntitiesApi.toggleStarred(mockEntityRef); - - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(true); - }); - - it('should unstar starred entity', async () => { - const bucket = mockStorage.forBucket('starredEntities'); - await bucket.set('entityRefs', ['component:default/mock']); - - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(true); - - await starredEntitiesApi.toggleStarred(mockEntityRef); - - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(false); - }); - }); - - describe('starredEntities$', () => { - const handler = jest.fn(); - - beforeEach(async () => { - await new Promise(resolve => { - starredEntitiesApi.starredEntitie$().subscribe({ - next: (...args) => { - handler(...args); - - if (handler.mock.calls.length >= 2) { - resolve(); - } - }, - }); - - const bucket = mockStorage.forBucket('starredEntities'); - bucket.set('entityRefs', ['component:default/mock']).then(); - }); - }); - - it('should receive updates', async () => { - expect(handler).toBeCalledTimes(2); - expect(handler).toBeCalledWith(new Set()); - expect(handler).toBeCalledWith(new Set(['component:default/mock'])); - }); - }); -}); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts new file mode 100644 index 0000000000..aadecefe98 --- /dev/null +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { MockStarredEntitiesApi } from './MockStarredEntitiesApi'; + +describe('MockStarredEntitiesApi', () => { + it('should toggle starred entities', async () => { + const api = new MockStarredEntitiesApi(); + + const updates1 = new Array>(); + const sub1 = api + .starredEntitie$() + .subscribe(entities => updates1.push(entities)); + + api.toggleStarred('k:ns/e1'); + api.toggleStarred('k:ns/e2'); + + await Promise.resolve(); + expect(updates1).toEqual([ + new Set(), + new Set(['k:ns/e1']), + new Set(['k:ns/e1', 'k:ns/e2']), + ]); + + const updates2 = new Array>(); + const sub2 = api + .starredEntitie$() + .subscribe(entities => updates2.push(entities)); + + api.toggleStarred('k:ns/e2'); + sub1.unsubscribe(); + api.toggleStarred('k:ns/e2'); + + await Promise.resolve(); + expect(updates1).toEqual([ + new Set(), + new Set(['k:ns/e1']), + new Set(['k:ns/e1', 'k:ns/e2']), + new Set(['k:ns/e1']), + ]); + expect(updates2).toEqual([ + new Set(['k:ns/e1', 'k:ns/e2']), + new Set(['k:ns/e1']), + new Set(['k:ns/e1', 'k:ns/e2']), + ]); + sub2.unsubscribe(); + }); +}); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts new file mode 100644 index 0000000000..9467153dd9 --- /dev/null +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts @@ -0,0 +1,54 @@ +/* + * 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 { Observable } from '@backstage/types'; +import ObservableImpl from 'zen-observable'; +import { StarredEntitiesApi } from './StarredEntitiesApi'; + +/** + * An in-memory mock implementation of the StarredEntitiesApi. + * + * @public + */ +export class MockStarredEntitiesApi implements StarredEntitiesApi { + private readonly starredEntities = new Set(); + private readonly subscribers = new Set< + ZenObservable.SubscriptionObserver> + >(); + + private readonly observable = new ObservableImpl>(subscriber => { + subscriber.next(new Set(this.starredEntities)); + + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }); + + async toggleStarred(entityRef: string): Promise { + if (!this.starredEntities.delete(entityRef)) { + this.starredEntities.add(entityRef); + } + + for (const subscription of this.subscribers) { + subscription.next(new Set(this.starredEntities)); + } + } + + starredEntitie$(): Observable> { + return this.observable; + } +} diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts index e9f9c8923a..ba44c38e1d 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; export { starredEntitiesApiRef } from './StarredEntitiesApi'; export type { StarredEntitiesApi } from './StarredEntitiesApi'; +export { MockStarredEntitiesApi } from './MockStarredEntitiesApi'; diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx index 17d28aad9f..1891f83df7 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx @@ -16,7 +16,7 @@ import React, { useEffect, useState } from 'react'; import { Alert } from '@material-ui/lab'; -import { useEntityListProvider } from '../../hooks'; +import { useEntityList } from '../../hooks'; import { EntityKindFilter } from '../../filters'; /** @@ -33,7 +33,7 @@ export interface EntityKindPickerProps { export const EntityKindPicker = (props: EntityKindPickerProps) => { const { initialFilter, hidden } = props; - const { updateFilters, queryParameters } = useEntityListProvider(); + const { updateFilters, queryParameters } = useEntityList(); const [selectedKind] = useState( [queryParameters.kind].flat()[0] ?? initialFilter, ); diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index 77691ab7c5..a05914fd6d 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -28,7 +28,7 @@ import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useEffect, useMemo, useState } from 'react'; -import { useEntityListProvider } from '../../hooks/useEntityListProvider'; +import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityLifecycleFilter } from '../../filters'; /** @public */ @@ -50,7 +50,7 @@ const checkedIcon = ; export const EntityLifecyclePicker = () => { const classes = useStyles(); const { updateFilters, backendEntities, filters, queryParameters } = - useEntityListProvider(); + useEntityList(); const queryParamLifecycles = useMemo( () => [queryParameters.lifecycles].flat().filter(Boolean) as string[], diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx index 14abc358db..de21bfeee3 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { Entity, parseEntityRef } from '@backstage/catalog-model'; import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; @@ -31,6 +31,7 @@ const sampleEntities: Entity[] = [ relations: [ { type: 'ownedBy', + targetRef: 'group:default/some-owner', target: { name: 'some-owner', namespace: 'default', @@ -39,6 +40,7 @@ const sampleEntities: Entity[] = [ }, { type: 'ownedBy', + targetRef: 'group:default/some-owner-2', target: { name: 'some-owner-2', namespace: 'default', @@ -56,6 +58,7 @@ const sampleEntities: Entity[] = [ relations: [ { type: 'ownedBy', + targetRef: 'group:default/another-owner', target: { name: 'another-owner', namespace: 'default', @@ -73,6 +76,7 @@ const sampleEntities: Entity[] = [ relations: [ { type: 'ownedBy', + targetRef: 'group:default/some-owner', target: { name: 'some-owner', namespace: 'default', @@ -96,7 +100,7 @@ describe('', () => { fireEvent.click(rendered.getByTestId('owner-picker-expand')); sampleEntities - .flatMap(e => e.relations?.map(r => r.target.name)) + .flatMap(e => e.relations?.map(r => parseEntityRef(r.targetRef).name)) .forEach(owner => { expect(rendered.getByText(owner as string)).toBeInTheDocument(); }); diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index acb5ee9279..ec2c8126e5 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -28,10 +28,10 @@ import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useEffect, useMemo, useState } from 'react'; -import { useEntityListProvider } from '../../hooks/useEntityListProvider'; +import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityOwnerFilter } from '../../filters'; import { getEntityRelations } from '../../utils'; -import { formatEntityRefTitle } from '../EntityRefLink'; +import { humanizeEntityRef } from '../EntityRefLink'; /** @public */ export type CatalogReactEntityOwnerPickerClassKey = 'input'; @@ -52,7 +52,7 @@ const checkedIcon = ; export const EntityOwnerPicker = () => { const classes = useStyles(); const { updateFilters, backendEntities, filters, queryParameters } = - useEntityListProvider(); + useEntityList(); const queryParamOwners = useMemo( () => [queryParameters.owners].flat().filter(Boolean) as string[], @@ -86,7 +86,7 @@ export const EntityOwnerPicker = () => { backendEntities .flatMap((e: Entity) => getEntityRelations(e, RELATION_OWNED_BY).map(o => - formatEntityRefTitle(o, { defaultKind: 'group' }), + humanizeEntityRef(o, { defaultKind: 'group' }), ), ) .filter(Boolean) as string[], diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 8120ececd2..4db48351a9 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -17,11 +17,12 @@ import { Entity, EntityName, - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, + parseEntityRef, } from '@backstage/catalog-model'; import React, { forwardRef } from 'react'; import { entityRouteRef } from '../../routes'; -import { formatEntityRefTitle } from './format'; +import { humanizeEntityRef } from './humanize'; import { Link, LinkProps } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Tooltip } from '@material-ui/core'; @@ -32,7 +33,7 @@ import { Tooltip } from '@material-ui/core'; * @public */ export type EntityRefLinkProps = { - entityRef: Entity | EntityName; + entityRef: Entity | EntityName | string; defaultKind?: string; title?: string; children?: React.ReactNode; @@ -52,7 +53,12 @@ export const EntityRefLink = forwardRef( let namespace; let name; - if ('metadata' in entityRef) { + if (typeof entityRef === 'string') { + const parsed = parseEntityRef(entityRef); + kind = parsed.kind; + namespace = parsed.namespace; + name = parsed.name; + } else if ('metadata' in entityRef) { kind = entityRef.kind; namespace = entityRef.metadata.namespace; name = entityRef.metadata.name; @@ -63,16 +69,13 @@ export const EntityRefLink = forwardRef( } kind = kind.toLocaleLowerCase('en-US'); + namespace = namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE; - const routeParams = { - kind, - namespace: - namespace?.toLocaleLowerCase('en-US') ?? ENTITY_DEFAULT_NAMESPACE, - name, - }; - const formattedEntityRefTitle = formatEntityRefTitle(entityRef, { - defaultKind, - }); + const routeParams = { kind, namespace, name }; + const formattedEntityRefTitle = humanizeEntityRef( + { kind, namespace, name }, + { defaultKind }, + ); const link = ( diff --git a/plugins/catalog-react/src/components/EntityRefLink/format.test.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts similarity index 86% rename from plugins/catalog-react/src/components/EntityRefLink/format.test.ts rename to plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts index b489f088fd..b541946ece 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/format.test.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { formatEntityRefTitle } from './format'; +import { humanizeEntityRef } from './humanize'; describe('formatEntityRefTitle', () => { it('formats entity in default namespace', () => { @@ -30,7 +30,7 @@ describe('formatEntityRefTitle', () => { lifecycle: 'production', }, }; - const title = formatEntityRefTitle(entity); + const title = humanizeEntityRef(entity); expect(title).toEqual('component:software'); }); @@ -48,7 +48,7 @@ describe('formatEntityRefTitle', () => { lifecycle: 'production', }, }; - const title = formatEntityRefTitle(entity); + const title = humanizeEntityRef(entity); expect(title).toEqual('component:test/software'); }); @@ -66,7 +66,7 @@ describe('formatEntityRefTitle', () => { lifecycle: 'production', }, }; - const title = formatEntityRefTitle(entity, { defaultKind: 'Component' }); + const title = humanizeEntityRef(entity, { defaultKind: 'Component' }); expect(title).toEqual('test/software'); }); @@ -76,7 +76,7 @@ describe('formatEntityRefTitle', () => { namespace: 'default', name: 'software', }; - const title = formatEntityRefTitle(entityName); + const title = humanizeEntityRef(entityName); expect(title).toEqual('component:software'); }); @@ -87,7 +87,7 @@ describe('formatEntityRefTitle', () => { name: 'software', }; - const title = formatEntityRefTitle(entityName); + const title = humanizeEntityRef(entityName); expect(title).toEqual('component:test/software'); }); @@ -98,7 +98,7 @@ describe('formatEntityRefTitle', () => { name: 'software', }; - const title = formatEntityRefTitle(entityName, { + const title = humanizeEntityRef(entityName, { defaultKind: 'component', }); expect(title).toEqual('test/software'); diff --git a/plugins/catalog-react/src/components/EntityRefLink/format.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts similarity index 73% rename from plugins/catalog-react/src/components/EntityRefLink/format.ts rename to plugins/catalog-react/src/components/EntityRefLink/humanize.ts index f3977ccc95..e2984c34ab 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/format.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -17,11 +17,14 @@ import { Entity, EntityName, - ENTITY_DEFAULT_NAMESPACE, - serializeEntityRef, + DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; -export function formatEntityRefTitle( +/** @public @deprecated please use {@link humanizeEntityRef} instead */ +export const formatEntityRefTitle = humanizeEntityRef; + +/** @public */ +export function humanizeEntityRef( entityRef: Entity | EntityName, opts?: { defaultKind?: string }, ) { @@ -40,18 +43,14 @@ export function formatEntityRefTitle( name = entityRef.name; } - if (namespace === ENTITY_DEFAULT_NAMESPACE) { + if (namespace === DEFAULT_NAMESPACE) { namespace = undefined; } 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 d49993feb6..50394547a0 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/index.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/index.ts @@ -18,4 +18,4 @@ export { EntityRefLink } from './EntityRefLink'; export type { EntityRefLinkProps } from './EntityRefLink'; export { EntityRefLinks } from './EntityRefLinks'; export type { EntityRefLinksProps } from './EntityRefLinks'; -export { formatEntityRefTitle } from './format'; +export { humanizeEntityRef, formatEntityRefTitle } from './humanize'; diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx index ec977fada4..fdb07658d0 100644 --- a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx @@ -26,7 +26,7 @@ import Clear from '@material-ui/icons/Clear'; import Search from '@material-ui/icons/Search'; import React, { useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; -import { useEntityListProvider } from '../../hooks/useEntityListProvider'; +import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityTextFilter } from '../../filters'; /** @public */ @@ -45,11 +45,14 @@ const useStyles = makeStyles( }, ); -/** @public */ +/** + * Renders search bar for filtering the entity list. + * @public + */ export const EntitySearchBar = () => { const classes = useStyles(); - const { filters, updateFilters } = useEntityListProvider(); + const { filters, updateFilters } = useEntityList(); const [search, setSearch] = useState(filters.text?.value ?? ''); useDebounce( diff --git a/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx index 62c33701b4..879a35ffd0 100644 --- a/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx +++ b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { makeStyles } from '@material-ui/core'; import React, { ReactNode } from 'react'; -import * as columnFactories from './columns'; +import { columnFactories } from './columns'; import { componentEntityColumns, systemEntityColumns } from './presets'; import { Table, TableColumn } from '@backstage/core-components'; @@ -48,7 +48,7 @@ const useStyles = makeStyles(theme => ({ * * @public */ -export function EntityTable(props: EntityTableProps) { +export const EntityTable = (props: EntityTableProps) => { const { entities, title, @@ -85,7 +85,7 @@ export function EntityTable(props: EntityTableProps) { data={entities} /> ); -} +}; EntityTable.columns = columnFactories; diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index f6198fa1dc..d5ef791831 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -26,154 +26,140 @@ import { getEntityRelations } from '../../utils'; import { EntityRefLink, EntityRefLinks, - formatEntityRefTitle, + humanizeEntityRef, } from '../EntityRefLink'; /** @public */ -export function createEntityRefColumn(options: { - defaultKind?: string; -}): TableColumn { - const { defaultKind } = options; - function formatContent(entity: T): string { - return ( - entity.metadata?.title || - formatEntityRefTitle(entity, { - defaultKind, - }) - ); - } - - return { - title: 'Name', - highlight: true, - customFilterAndSearch(filter, entity) { - // TODO: We could implement this more efficiently, like searching over - // each field that is displayed individually (kind, namespace, name). - // 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. - return formatContent(entity).includes(filter); - }, - customSort(entity1, entity2) { - // TODO: We could implement this more efficiently by comparing field by field. - // This has similar issues as above. - return formatContent(entity1).localeCompare(formatContent(entity2)); - }, - render: entity => ( - - ), - }; -} - -/** @public */ -export function createEntityRelationColumn({ - title, - relation, - defaultKind, - filter: entityFilter, -}: { - title: string; - relation: string; - defaultKind?: string; - filter?: { kind: string }; -}): TableColumn { - function getRelations(entity: T): EntityName[] { - return getEntityRelations(entity, relation, entityFilter); - } - - function formatContent(entity: T): string { - return getRelations(entity) - .map(r => formatEntityRefTitle(r, { defaultKind })) - .join(', '); - } - - return { - title, - customFilterAndSearch(filter, entity) { - return formatContent(entity).includes(filter); - }, - customSort(entity1, entity2) { - return formatContent(entity1).localeCompare(formatContent(entity2)); - }, - render: entity => { +export const columnFactories = Object.freeze({ + createEntityRefColumn(options: { + defaultKind?: string; + }): TableColumn { + const { defaultKind } = options; + function formatContent(entity: T): string { return ( - + entity.metadata?.title || + humanizeEntityRef(entity, { + defaultKind, + }) ); - }, - }; -} + } -/** @public */ -export function createOwnerColumn(): TableColumn { - return createEntityRelationColumn({ - title: 'Owner', - relation: RELATION_OWNED_BY, - defaultKind: 'group', - }); -} + return { + title: 'Name', + highlight: true, + customFilterAndSearch(filter, entity) { + // TODO: We could implement this more efficiently, like searching over + // each field that is displayed individually (kind, namespace, name). + // 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. + return formatContent(entity).includes(filter); + }, + customSort(entity1, entity2) { + // TODO: We could implement this more efficiently by comparing field by field. + // This has similar issues as above. + return formatContent(entity1).localeCompare(formatContent(entity2)); + }, + render: entity => ( + + ), + }; + }, + createEntityRelationColumn({ + title, + relation, + defaultKind, + filter: entityFilter, + }: { + title: string; + relation: string; + defaultKind?: string; + filter?: { kind: string }; + }): TableColumn { + function getRelations(entity: T): EntityName[] { + return getEntityRelations(entity, relation, entityFilter); + } -/** @public */ -export function createDomainColumn(): TableColumn { - return createEntityRelationColumn({ - title: 'Domain', - relation: RELATION_PART_OF, - defaultKind: 'domain', - filter: { - kind: 'domain', - }, - }); -} + function formatContent(entity: T): string { + return getRelations(entity) + .map(r => humanizeEntityRef(r, { defaultKind })) + .join(', '); + } -/** @public */ -export function createSystemColumn(): TableColumn { - return createEntityRelationColumn({ - title: 'System', - relation: RELATION_PART_OF, - defaultKind: 'system', - filter: { - kind: 'system', - }, - }); -} - -/** @public */ -export function createMetadataDescriptionColumn< - T extends Entity, ->(): TableColumn { - return { - title: 'Description', - field: 'metadata.description', - render: entity => ( - - ), - width: 'auto', - }; -} - -/** @public */ -export function createSpecLifecycleColumn(): TableColumn { - return { - title: 'Lifecycle', - field: 'spec.lifecycle', - }; -} - -/** @public */ -export function createSpecTypeColumn(): TableColumn { - return { - title: 'Type', - field: 'spec.type', - }; -} + return { + title, + customFilterAndSearch(filter, entity) { + return formatContent(entity).includes(filter); + }, + customSort(entity1, entity2) { + return formatContent(entity1).localeCompare(formatContent(entity2)); + }, + render: entity => { + return ( + + ); + }, + }; + }, + createOwnerColumn(): TableColumn { + return this.createEntityRelationColumn({ + title: 'Owner', + relation: RELATION_OWNED_BY, + defaultKind: 'group', + }); + }, + createDomainColumn(): TableColumn { + return this.createEntityRelationColumn({ + title: 'Domain', + relation: RELATION_PART_OF, + defaultKind: 'domain', + filter: { + kind: 'domain', + }, + }); + }, + createSystemColumn(): TableColumn { + return this.createEntityRelationColumn({ + title: 'System', + relation: RELATION_PART_OF, + defaultKind: 'system', + filter: { + kind: 'system', + }, + }); + }, + createMetadataDescriptionColumn(): TableColumn { + return { + title: 'Description', + field: 'metadata.description', + render: entity => ( + + ), + width: 'auto', + }; + }, + createSpecLifecycleColumn(): TableColumn { + return { + title: 'Lifecycle', + field: 'spec.lifecycle', + }; + }, + createSpecTypeColumn(): TableColumn { + return { + title: 'Type', + field: 'spec.type', + }; + }, +}); diff --git a/plugins/catalog-react/src/components/EntityTable/index.ts b/plugins/catalog-react/src/components/EntityTable/index.ts index 2ed07f9823..5fb651fe72 100644 --- a/plugins/catalog-react/src/components/EntityTable/index.ts +++ b/plugins/catalog-react/src/components/EntityTable/index.ts @@ -15,4 +15,5 @@ */ export { EntityTable } from './EntityTable'; +export { columnFactories } from './columns'; export type { EntityTableProps } from './EntityTable'; diff --git a/plugins/catalog-react/src/components/EntityTable/presets.test.tsx b/plugins/catalog-react/src/components/EntityTable/presets.test.tsx index a509b67477..f2fcd016f0 100644 --- a/plugins/catalog-react/src/components/EntityTable/presets.test.tsx +++ b/plugins/catalog-react/src/components/EntityTable/presets.test.tsx @@ -44,17 +44,19 @@ describe('systemEntityColumns', () => { relations: [ { type: RELATION_PART_OF, + targetRef: 'domain:my-namespace/my-domain', target: { - kind: 'Domain', + kind: 'domain', name: 'my-domain', namespace: 'my-namespace', }, }, { type: RELATION_OWNED_BY, + targetRef: 'group:default/test', target: { - kind: 'Group', - name: 'Test', + kind: 'group', + name: 'test', namespace: 'default', }, }, @@ -79,7 +81,7 @@ describe('systemEntityColumns', () => { await waitFor(() => { expect(getByText('my-namespace/my-system')).toBeInTheDocument(); expect(getByText('my-namespace/my-domain')).toBeInTheDocument(); - expect(getByText('Test')).toBeInTheDocument(); + expect(getByText('test')).toBeInTheDocument(); expect(getByText(/Some/)).toBeInTheDocument(); }); }); @@ -104,17 +106,19 @@ describe('componentEntityColumns', () => { relations: [ { type: RELATION_PART_OF, + targetRef: 'system:my-namespace/my-system', target: { - kind: 'System', + kind: 'system', name: 'my-system', namespace: 'my-namespace', }, }, { type: RELATION_OWNED_BY, + targetRef: 'group:default/test', target: { - kind: 'Group', - name: 'Test', + kind: 'group', + name: 'test', namespace: 'default', }, }, @@ -139,7 +143,7 @@ describe('componentEntityColumns', () => { await waitFor(() => { expect(getByText('my-namespace/my-component')).toBeInTheDocument(); expect(getByText('my-namespace/my-system')).toBeInTheDocument(); - expect(getByText('Test')).toBeInTheDocument(); + expect(getByText('test')).toBeInTheDocument(); expect(getByText('production')).toBeInTheDocument(); expect(getByText('service')).toBeInTheDocument(); expect(getByText(/Some/)).toBeInTheDocument(); diff --git a/plugins/catalog-react/src/components/EntityTable/presets.tsx b/plugins/catalog-react/src/components/EntityTable/presets.tsx index 2ddffb0b7a..6e4ddb2081 100644 --- a/plugins/catalog-react/src/components/EntityTable/presets.tsx +++ b/plugins/catalog-react/src/components/EntityTable/presets.tsx @@ -15,29 +15,21 @@ */ import { ComponentEntity, SystemEntity } from '@backstage/catalog-model'; -import { - createDomainColumn, - createEntityRefColumn, - createMetadataDescriptionColumn, - createOwnerColumn, - createSpecLifecycleColumn, - createSpecTypeColumn, - createSystemColumn, -} from './columns'; import { TableColumn } from '@backstage/core-components'; +import { columnFactories } from './columns'; export const systemEntityColumns: TableColumn[] = [ - createEntityRefColumn({ defaultKind: 'system' }), - createDomainColumn(), - createOwnerColumn(), - createMetadataDescriptionColumn(), + columnFactories.createEntityRefColumn({ defaultKind: 'system' }), + columnFactories.createDomainColumn(), + columnFactories.createOwnerColumn(), + columnFactories.createMetadataDescriptionColumn(), ]; export const componentEntityColumns: TableColumn[] = [ - createEntityRefColumn({ defaultKind: 'component' }), - createSystemColumn(), - createOwnerColumn(), - createSpecTypeColumn(), - createSpecLifecycleColumn(), - createMetadataDescriptionColumn(), + columnFactories.createEntityRefColumn({ defaultKind: 'component' }), + columnFactories.createSystemColumn(), + columnFactories.createOwnerColumn(), + columnFactories.createSpecTypeColumn(), + columnFactories.createSpecLifecycleColumn(), + columnFactories.createMetadataDescriptionColumn(), ]; diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index a91389a653..5b5e0e7ab9 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -28,7 +28,7 @@ import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useEffect, useMemo, useState } from 'react'; -import { useEntityListProvider } from '../../hooks/useEntityListProvider'; +import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityTagFilter } from '../../filters'; /** @public */ @@ -50,7 +50,7 @@ const checkedIcon = ; export const EntityTagPicker = () => { const classes = useStyles(); const { updateFilters, backendEntities, filters, queryParameters } = - useEntityListProvider(); + useEntityList(); const queryParamTags = useMemo( () => [queryParameters.tags].flat().filter(Boolean) as string[], diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index 9ed27bbef8..4623cf5d9e 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -26,6 +26,7 @@ import { EntityKindFilter, EntityTypeFilter } from '../../filters'; import { alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider } from '@backstage/core-app-api'; import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; +import { GetEntityFacetsResponse } from '@backstage/catalog-client'; const entities: Entity[] = [ { @@ -64,7 +65,14 @@ const apis = TestApiRegistry.from( [ catalogApiRef, { - getEntities: jest.fn().mockResolvedValue({ items: entities }), + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { + 'spec.type': entities.map(e => ({ + value: (e.spec as any).type, + count: 1, + })), + }, + } as GetEntityFacetsResponse), }, ], [ diff --git a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx index 730c9a2378..3832a82877 100644 --- a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx +++ b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx @@ -21,7 +21,10 @@ import StarBorder from '@material-ui/icons/StarBorder'; import React, { ComponentProps } from 'react'; import { useStarredEntity } from '../../hooks/useStarredEntity'; -type Props = ComponentProps & { entity: Entity }; +/** @public */ +export type FavoriteEntityProps = ComponentProps & { + entity: Entity; +}; const YellowStar = withStyles({ root: { @@ -29,17 +32,20 @@ const YellowStar = withStyles({ }, })(Star); +/** @public */ export const favoriteEntityTooltip = (isStarred: boolean) => isStarred ? 'Remove from favorites' : 'Add to favorites'; +/** @public */ export const favoriteEntityIcon = (isStarred: boolean) => isStarred ? : ; /** * IconButton for showing if a current entity is starred and adding/removing it from the favorite entities * @param props - MaterialUI IconButton props extended by required `entity` prop + * @public */ -export const FavoriteEntity = (props: Props) => { +export const FavoriteEntity = (props: FavoriteEntityProps) => { const { toggleStarredEntity, isStarredEntity } = useStarredEntity( props.entity, ); diff --git a/plugins/catalog-react/src/components/FavoriteEntity/index.ts b/plugins/catalog-react/src/components/FavoriteEntity/index.ts index f731ca7483..a46d04b6c7 100644 --- a/plugins/catalog-react/src/components/FavoriteEntity/index.ts +++ b/plugins/catalog-react/src/components/FavoriteEntity/index.ts @@ -19,3 +19,4 @@ export { favoriteEntityIcon, FavoriteEntity, } from './FavoriteEntity'; +export type { FavoriteEntityProps } from './FavoriteEntity'; diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx index b27bbe3948..0423c0da44 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx @@ -16,7 +16,7 @@ import { Entity, - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; import { @@ -33,7 +33,7 @@ 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 { humanizeEntityRef } from '../../EntityRefLink'; import { entityRouteRef } from '../../../routes'; import { EntityKindIcon } from './EntityKindIcon'; @@ -132,7 +132,7 @@ function CustomNode({ node }: DependencyGraphTypes.RenderNodeProps) { const displayTitle = node.metadata.title || (node.kind && node.metadata.name && node.metadata.namespace - ? formatEntityRefTitle({ + ? humanizeEntityRef({ kind: node.kind, name: node.metadata.name, namespace: node.metadata.namespace || '', @@ -143,7 +143,7 @@ function CustomNode({ node }: DependencyGraphTypes.RenderNodeProps) { navigate( entityRoute({ kind: node.kind, - namespace: node.metadata.namespace || ENTITY_DEFAULT_NAMESPACE, + namespace: node.metadata.namespace || DEFAULT_NAMESPACE, name: node.metadata.name, }), ); diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx index 96d3900a59..d5a98f434c 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx @@ -16,8 +16,8 @@ import { Entity, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, stringifyEntityRef, } from '@backstage/catalog-model'; import { Progress, ResponseErrorPanel } from '@backstage/core-components'; @@ -53,8 +53,8 @@ function useColocated(entity: Entity): { } { const catalogApi = useApi(catalogApiRef); const currentEntityRef = stringifyEntityRef(entity); - const location = entity.metadata.annotations?.[LOCATION_ANNOTATION]; - const origin = entity.metadata.annotations?.[ORIGIN_LOCATION_ANNOTATION]; + const location = entity.metadata.annotations?.[ANNOTATION_LOCATION]; + const origin = entity.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION]; const { loading, error, value } = useAsync(async () => { if (!location && !origin) { @@ -63,10 +63,10 @@ function useColocated(entity: Entity): { const response = await catalogApi.getEntities({ filter: [ ...(location - ? [{ [`metadata.annotations.${LOCATION_ANNOTATION}`]: location }] + ? [{ [`metadata.annotations.${ANNOTATION_LOCATION}`]: location }] : []), ...(origin - ? [{ [`metadata.annotations.${ORIGIN_LOCATION_ANNOTATION}`]: origin }] + ? [{ [`metadata.annotations.${ANNOTATION_ORIGIN_LOCATION}`]: origin }] : []), ], }); @@ -128,11 +128,11 @@ function Contents(props: { entity: Entity }) { } const atLocation = colocatedEntities.filter( - e => e.metadata.annotations?.[LOCATION_ANNOTATION] === location, + e => e.metadata.annotations?.[ANNOTATION_LOCATION] === location, ); const atOrigin = colocatedEntities.filter( e => - e.metadata.annotations?.[ORIGIN_LOCATION_ANNOTATION] === originLocation, + e.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION] === originLocation, ); return ( diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/EntityKindIcon.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/EntityKindIcon.tsx index c2fc49aa0c..4bb41c91c1 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/EntityKindIcon.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/EntityKindIcon.tsx @@ -14,23 +14,54 @@ * limitations under the License. */ +import { parseEntityRef } from '@backstage/catalog-model'; import { useApp } from '@backstage/core-plugin-api'; import WorkIcon from '@material-ui/icons/Work'; import React from 'react'; +const DEFAULT_ICON = WorkIcon; + +function getKind( + kind: string | undefined, + entityRef: string | undefined, +): string | undefined { + if (kind) { + return kind.toLocaleLowerCase('en-US'); + } + + if (entityRef) { + try { + return parseEntityRef(entityRef).kind.toLocaleLowerCase('en-US'); + } catch { + return undefined; + } + } + + return undefined; +} + +function useIcon(kind: string | undefined, entityRef: string | undefined) { + const app = useApp(); + + const actualKind = getKind(kind, entityRef); + if (!actualKind) { + return DEFAULT_ICON; + } + + const icon = app.getSystemIcon(`kind:${actualKind}`); + return icon || DEFAULT_ICON; +} + export function EntityKindIcon(props: { - kind: string; + kind?: string; + entityRef?: 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; - + const { kind, entityRef, ...otherProps } = props; + const Icon = useIcon(kind, entityRef); return ; } diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx index dbb7399481..a4e8523beb 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AlphaEntity, stringifyEntityRef } from '@backstage/catalog-model'; +import { AlphaEntity } from '@backstage/catalog-model'; import { Box, DialogContentText, @@ -56,7 +56,7 @@ export function OverviewPage(props: { entity: AlphaEntity }) { } = props.entity; const groupedRelations = groupBy( - sortBy(relations, r => stringifyEntityRef(r.target)), + sortBy(relations, r => r.targetRef), 'type', ); @@ -143,12 +143,14 @@ export function OverviewPage(props: { entity: AlphaEntity }) {
{type}}> {groupRelations.map(group => ( - + - + } + primary={ + + } /> ))} 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/UnregisterEntityDialog.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index 5c267916ea..48aa2f4012 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -41,13 +41,6 @@ const useStyles = makeStyles({ }, }); -type Props = { - open: boolean; - onConfirm: () => any; - onClose: () => any; - entity: Entity; -}; - const Contents = ({ entity, onConfirm, @@ -248,23 +241,30 @@ const Contents = ({ return Internal error: Unknown state; }; -export const UnregisterEntityDialog = ({ - open, - onConfirm, - onClose, - entity, -}: Props) => ( - - - Are you sure you want to unregister this entity? - - - - - - - - -); +/** @public */ +export type UnregisterEntityDialogProps = { + open: boolean; + onConfirm: () => any; + onClose: () => any; + entity: Entity; +}; + +/** @public */ +export const UnregisterEntityDialog = (props: UnregisterEntityDialogProps) => { + const { open, onConfirm, onClose, entity } = props; + return ( + + + Are you sure you want to unregister this entity? + + + + + + + + + ); +}; diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/index.ts b/plugins/catalog-react/src/components/UnregisterEntityDialog/index.ts index 8fc750e706..66eb6961ae 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/index.ts +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/index.ts @@ -15,3 +15,4 @@ */ export { UnregisterEntityDialog } from './UnregisterEntityDialog'; +export type { UnregisterEntityDialogProps } from './UnregisterEntityDialog'; diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx index 8d6083f111..25af7564d1 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, @@ -43,7 +39,7 @@ function defer(): { promise: Promise; resolve: (value: T) => void } { describe('useUnregisterEntityDialogState', () => { const catalogApiMock = { - getOriginLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), getEntities: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), @@ -69,9 +65,7 @@ describe('useUnregisterEntityDialogState', () => { resolveLocation = deferredLocation.resolve; resolveColocatedEntities = deferredColocatedEntities.resolve; - catalogApiMock.getOriginLocationByEntity.mockReturnValue( - deferredLocation.promise, - ); + catalogApiMock.getLocationByRef.mockReturnValue(deferredLocation.promise); catalogApiMock.getEntities.mockReturnValue( deferredColocatedEntities.promise.then(items => ({ items })), ); @@ -83,7 +77,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 +111,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 +135,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..6420ebd5ae 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,20 +60,20 @@ 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'; // Load the prerequisite data: what entities that are colocated with us, and // what location that spawned us const prerequisites = useAsync(async () => { - const locationPromise = catalogApi.getOriginLocationByEntity(entity); + const locationPromise = catalogApi.getLocationByRef(locationRef!); let colocatedEntitiesPromise: Promise; 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 d4cfedecbf..316a0f2c06 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -36,6 +36,7 @@ import { identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; +import { useEntityOwnership } from '../../hooks'; const mockUser: UserEntity = { apiVersion: 'backstage.io/v1alpha1', @@ -81,9 +82,9 @@ jest.mock('../../hooks', () => { const actual = jest.requireActual('../../hooks'); return { ...actual, - useEntityOwnership: () => ({ + useEntityOwnership: jest.fn(() => ({ isOwnedEntity: mockIsOwnedEntity, - }), + })), useStarredEntities: () => ({ isStarredEntity: mockIsStarredEntity, }), @@ -102,6 +103,7 @@ const backendEntities: Entity[] = [ relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/testuser', target: { kind: 'User', namespace: 'default', name: 'testUser' }, }, ], @@ -135,6 +137,7 @@ const backendEntities: Entity[] = [ relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/testuser', target: { kind: 'User', namespace: 'default', name: 'testUser' }, }, ], @@ -327,6 +330,19 @@ describe('', () => { }); }); + it('does not reset the filter while owned entities are loading', () => { + const isOwnedEntity = jest.fn(() => false); + (useEntityOwnership as jest.Mock).mockReturnValueOnce({ + loading: true, + isOwnedEntity, + }); + + render(picker({ loading: false })); + expect(updateFilters).not.toHaveBeenCalledWith({ + user: new UserListFilter('all', isOwnedEntity, mockIsStarredEntity), + }); + }); + it('resets the filter to "all" when entities are loaded', () => { render(picker({ loading: false })); diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index cbf6089f2e..d281bb31a8 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -36,7 +36,7 @@ import { compact } from 'lodash'; import React, { Fragment, useEffect, useMemo, useState } from 'react'; import { UserListFilter } from '../../filters'; import { - useEntityListProvider, + useEntityList, useStarredEntities, useEntityOwnership, } from '../../hooks'; @@ -118,20 +118,25 @@ function getFilterGroups(orgName: string | undefined): ButtonGroup[] { ]; } -type UserListPickerProps = { +/** @public */ +export type UserListPickerProps = { initialFilter?: UserListFilterKind; availableFilters?: UserListFilterKind[]; }; -export const UserListPicker = ({ - initialFilter, - availableFilters, -}: UserListPickerProps) => { +/** @public */ +export const UserListPicker = (props: UserListPickerProps) => { + const { initialFilter, availableFilters } = props; const classes = useStyles(); const configApi = useApi(configApiRef); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; - const { filters, updateFilters, backendEntities, queryParameters, loading } = - useEntityListProvider(); + const { + filters, + updateFilters, + backendEntities, + queryParameters, + loading: loadingBackendEntities, + } = useEntityList(); // Remove group items that aren't in availableFilters and exclude // any now-empty groups. @@ -149,7 +154,10 @@ export const UserListPicker = ({ .filter(({ items }) => !!items.length); const { isStarredEntity } = useStarredEntities(); - const { isOwnedEntity } = useEntityOwnership(); + const { isOwnedEntity, loading: loadingEntityOwnership } = + useEntityOwnership(); + + const loading = loadingBackendEntities || loadingEntityOwnership; // Static filters; used for generating counts of potentially unselected kinds const ownedFilter = useMemo( diff --git a/plugins/catalog-react/src/components/UserListPicker/index.ts b/plugins/catalog-react/src/components/UserListPicker/index.ts index 45cb471197..269c4ca304 100644 --- a/plugins/catalog-react/src/components/UserListPicker/index.ts +++ b/plugins/catalog-react/src/components/UserListPicker/index.ts @@ -15,4 +15,5 @@ */ export { UserListPicker } from './UserListPicker'; +export type { UserListPickerProps } from './UserListPicker'; export type { CatalogReactUserListPickerClassKey } from './UserListPicker'; diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts index 668e3b4bc3..cdc2ce2150 100644 --- a/plugins/catalog-react/src/filters.test.ts +++ b/plugins/catalog-react/src/filters.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Entity, TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; import { EntityTextFilter } from './filters'; const entities: Entity[] = [ diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 2804d5d6bd..9fb057a01c 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -15,10 +15,14 @@ */ import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { formatEntityRefTitle } from './components/EntityRefLink'; +import { humanizeEntityRef } from './components/EntityRefLink'; import { EntityFilter, UserListFilterKind } from './types'; import { getEntityRelations } from './utils'; +/** + * Filter entities based on Kind. + * @public + */ export class EntityKindFilter implements EntityFilter { constructor(readonly value: string) {} @@ -31,6 +35,10 @@ export class EntityKindFilter implements EntityFilter { } } +/** + * Filters entities based on type + * @public + */ export class EntityTypeFilter implements EntityFilter { constructor(readonly value: string | string[]) {} @@ -48,6 +56,10 @@ export class EntityTypeFilter implements EntityFilter { } } +/** + * Filters entities based on tag. + * @public + */ export class EntityTagFilter implements EntityFilter { constructor(readonly values: string[]) {} @@ -60,6 +72,10 @@ export class EntityTagFilter implements EntityFilter { } } +/** + * Filters entities where the text matches spec, title or tags. + * @public + */ export class EntityTextFilter implements EntityFilter { constructor(readonly value: string) {} @@ -81,13 +97,17 @@ export class EntityTextFilter implements EntityFilter { } } +/** + * Filter matching entities that are owned by group. + * @public + */ export class EntityOwnerFilter implements EntityFilter { constructor(readonly values: string[]) {} filterEntity(entity: Entity): boolean { return this.values.some(v => getEntityRelations(entity, RELATION_OWNED_BY).some( - o => formatEntityRefTitle(o, { defaultKind: 'group' }) === v, + o => humanizeEntityRef(o, { defaultKind: 'group' }) === v, ), ); } @@ -97,6 +117,10 @@ export class EntityOwnerFilter implements EntityFilter { } } +/** + * Filters entities on lifecycle. + * @public + */ export class EntityLifecycleFilter implements EntityFilter { constructor(readonly values: string[]) {} @@ -109,6 +133,10 @@ export class EntityLifecycleFilter implements EntityFilter { } } +/** + * Filters entities based on whatever the user has starred or owns them. + * @public + */ export class UserListFilter implements EntityFilter { constructor( readonly value: UserListFilterKind, diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 06791f5706..da23d54ad7 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -14,13 +14,14 @@ * limitations under the License. */ export { - EntityContext, useEntity, useEntityFromUrl, EntityProvider, AsyncEntityProvider, + useAsyncEntity, } from './useEntity'; export type { + EntityLoadingStatus, EntityProviderProps, AsyncEntityProviderProps, } from './useEntity'; @@ -29,18 +30,19 @@ export { EntityListContext, EntityListProvider, useEntityListProvider, + useEntityList, +} from './useEntityListProvider'; +export type { + DefaultEntityFilters, + EntityListContextProps, } from './useEntityListProvider'; -export type { DefaultEntityFilters } from './useEntityListProvider'; export { useEntityTypeFilter } from './useEntityTypeFilter'; +export type { EntityTypeReturn } from './useEntityTypeFilter'; export { useEntityKinds } from './useEntityKinds'; export { useOwnUser } from './useOwnUser'; export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; export { useStarredEntity } from './useStarredEntity'; -export { - loadCatalogOwnerRefs, - useEntityOwnership, - loadIdentityOwnerRefs, -} from './useEntityOwnership'; +export { loadCatalogOwnerRefs, useEntityOwnership } from './useEntityOwnership'; export { useOwnedEntities } from './useOwnedEntities'; export { useEntityPermission } from './useEntityPermission'; diff --git a/plugins/catalog-react/src/hooks/useEntity.test.tsx b/plugins/catalog-react/src/hooks/useEntity.test.tsx index 9e73ed1475..f61283f2cf 100644 --- a/plugins/catalog-react/src/hooks/useEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.test.tsx @@ -16,12 +16,7 @@ import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { - useEntity, - EntityProvider, - AsyncEntityProvider, - EntityContext, -} from './useEntity'; +import { useEntity, EntityProvider, AsyncEntityProvider } from './useEntity'; import { Entity } from '@backstage/catalog-model'; describe('EntityProvider', () => { @@ -103,38 +98,3 @@ describe('AsyncEntityProvider', () => { expect(result.current.refresh).toBe(undefined); }); }); - -describe('EntityContext.Provider', () => { - it('should provide no entity', async () => { - const { result } = renderHook(() => useEntity(), { - wrapper: ({ children }) => ( - - ), - }); - - expect(result.current.entity).toBe(undefined); - expect(result.current.loading).toBe(false); - expect(result.current.error).toBe(undefined); - expect(result.current.refresh).toBe(undefined); - }); - - it('should provide an entity', async () => { - const entity = { kind: 'MyEntity' } as Entity; - const { result } = renderHook(() => useEntity(), { - wrapper: ({ children }) => ( - - ), - }); - - expect(result.current.entity).toBe(entity); - expect(result.current.loading).toBe(false); - expect(result.current.error).toBe(undefined); - expect(result.current.refresh).toBe(undefined); - }); -}); diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index 6ad61a5d81..da3fb8f3a5 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -20,39 +20,20 @@ import { createVersionedValueMap, useVersionedContext, } from '@backstage/version-bridge'; -import React, { - ReactNode, - useEffect, - createContext, - Provider, - Context, -} from 'react'; +import React, { ReactNode, useEffect } from 'react'; import { useNavigate } from 'react-router'; import useAsyncRetry from 'react-use/lib/useAsyncRetry'; import { catalogApiRef } from '../api'; import { useEntityCompoundName } from './useEntityCompoundName'; -type EntityLoadingStatus = { - entity?: Entity; +/** @public */ +export type EntityLoadingStatus = { + entity?: TEntity; loading: boolean; error?: Error; refresh?: VoidFunction; }; -/** - * @public - * @deprecated use `useEntity` and `EntityProvider` or `AsyncEntityProvider` instead. - */ -export const EntityContext: Context = - createContext({ - entity: undefined, - loading: true, - error: undefined, - refresh: () => {}, - }); -// We grab this for use in the new provider, since we're overriding it later on -const OldEntityProvider = EntityContext.Provider; - // This context has support for multiple concurrent versions of this package. // It is currently used in parallel with the old context in order to provide // a smooth transition, but will eventually be the only context we use. @@ -88,11 +69,9 @@ export const AsyncEntityProvider = ({ // We provide both the old and the new context, since // consumers might be doing things like `useContext(EntityContext)` return ( - - - {children} - - + + {children} + ); }; @@ -121,18 +100,9 @@ export const EntityProvider = ({ entity, children }: EntityProviderProps) => ( /> ); -// This is used for forwards compatibility with the new entity context -const CompatibilityProvider = ({ - value, - children, -}: { - value: EntityLoadingStatus; - children: ReactNode; -}) => { - return ; -}; -EntityContext.Provider = CompatibilityProvider as Provider; - +/** @public + * @deprecated will be deleted shortly due to low external usage, re-implement if needed. + */ export const useEntityFromUrl = (): EntityLoadingStatus => { const { kind, namespace, name } = useEntityCompoundName(); const navigate = useNavigate(); @@ -160,24 +130,25 @@ export const useEntityFromUrl = (): EntityLoadingStatus => { }; /** - * Grab the current entity from the context and its current loading state. + * Grab the current entity from the context, throws if the entity has not yet been loaded + * or is not available. * * @public */ -export function useEntity() { +export function useEntity(): { + entity: TEntity; + /** @deprecated use {@link useAsyncEntity} instead */ + loading: boolean; + /** @deprecated use {@link useAsyncEntity} instead */ + error?: Error; + /** @deprecated use {@link useAsyncEntity} instead */ + refresh?: VoidFunction; +} { const versionedHolder = useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context'); if (!versionedHolder) { - // TODO(Rugvip): Throw this once we fully migrate to the new context - // throw new Error('Entity context is not available'); - - return { - entity: undefined as unknown as T, - loading: true, - error: undefined, - refresh: () => {}, - }; + throw new Error('Entity context is not available'); } const value = versionedHolder.atVersion(1); @@ -185,6 +156,40 @@ export function useEntity() { throw new Error('EntityContext v1 not available'); } + if (!value.entity) { + // Once we have removed the additional fields from being returned we can drop this deprecation + // and move to the error instead. + // throw new Error('useEntity hook is being called outside of an EntityLayout where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead.'); + + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION: useEntity hook is being called outside of an EntityLayout where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead. This warning will be replaced with an error in future releases.', + ); + } + const { entity, loading, error, refresh } = value; - return { entity: entity as T, loading, error, refresh }; + return { entity: entity as TEntity, loading, error, refresh }; +} + +/** + * Grab the current entity from the context, provides loading state and errors, and the ability to refresh. + * + * @public + */ +export function useAsyncEntity< + TEntity extends Entity = Entity, +>(): EntityLoadingStatus { + const versionedHolder = + useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context'); + + if (!versionedHolder) { + throw new Error('Entity context is not available'); + } + const value = versionedHolder.atVersion(1); + if (!value) { + throw new Error('EntityContext v1 not available'); + } + + const { entity, loading, error, refresh } = value; + return { entity: entity as TEntity, loading, error, refresh }; } diff --git a/plugins/catalog-react/src/hooks/useEntityCompoundName.ts b/plugins/catalog-react/src/hooks/useEntityCompoundName.ts index 57e1c08a23..481c18f8a0 100644 --- a/plugins/catalog-react/src/hooks/useEntityCompoundName.ts +++ b/plugins/catalog-react/src/hooks/useEntityCompoundName.ts @@ -18,6 +18,8 @@ import { useRouteRefParams } from '@backstage/core-plugin-api'; /** * Grabs entity kind, namespace, and name from the location + * @public + * @deprecated use {@link @backstage/core-plugin-api#useRouteRefParams} instead */ export const useEntityCompoundName = () => { const { kind, namespace, name } = useRouteRefParams(entityRouteRef); diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx index cdcac8c194..c96301b3c4 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx @@ -16,45 +16,21 @@ import React, { PropsWithChildren } from 'react'; import { CatalogApi } from '@backstage/catalog-client'; -import { Entity } from '@backstage/catalog-model'; import { catalogApiRef } from '../api'; import { renderHook } from '@testing-library/react-hooks'; import { useEntityKinds } from './useEntityKinds'; import { TestApiProvider } from '@backstage/test-utils'; -const entities: Entity[] = [ - { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'component-1', - }, - }, - { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'component-2', - }, - }, - { - apiVersion: '1', - kind: 'Template', - metadata: { - name: 'template', - }, - }, - { - apiVersion: '1', - kind: 'System', - metadata: { - name: 'system', - }, - }, -]; - const mockCatalogApi: Partial = { - getEntities: jest.fn().mockResolvedValue({ items: entities }), + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { + kind: [ + { value: 'Template', count: 2 }, + { value: 'System', count: 1 }, + { value: 'Component', count: 3 }, + ], + }, + }), }; const wrapper = ({ children }: PropsWithChildren<{}>) => { @@ -66,24 +42,10 @@ const wrapper = ({ children }: PropsWithChildren<{}>) => { }; describe('useEntityKinds', () => { - it('does not return duplicate kinds', async () => { + it('gets entity kinds', async () => { const { result, waitForValueToChange } = renderHook( () => useEntityKinds(), - { - wrapper, - }, - ); - await waitForValueToChange(() => result.current); - expect(result.current.kinds).toBeDefined(); - expect(result.current.kinds!.length).toBe(3); - }); - - it('sorts entity kinds', async () => { - const { result, waitForValueToChange } = renderHook( - () => useEntityKinds(), - { - wrapper, - }, + { wrapper }, ); await waitForValueToChange(() => result.current); expect(result.current.kinds).toEqual(['Component', 'System', 'Template']); diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.ts b/plugins/catalog-react/src/hooks/useEntityKinds.ts index 1d5b2fec52..3dfc31eb88 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.ts +++ b/plugins/catalog-react/src/hooks/useEntityKinds.ts @@ -18,7 +18,11 @@ import useAsync from 'react-use/lib/useAsync'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '../api'; -// Retrieve a list of unique entity kinds present in the catalog +/** + * Retrieve a list of unique entity kinds present in the catalog + * @public + * @deprecated and will be removed due to low utility value. + */ export function useEntityKinds() { const catalogApi = useApi(catalogApiRef); @@ -27,11 +31,9 @@ export function useEntityKinds() { loading, value: kinds, } = useAsync(async () => { - const entities = await catalogApi - .getEntities({ fields: ['kind'] }) - .then(response => response.items); - - return [...new Set(entities.map(e => e.kind))].sort(); + return await catalogApi + .getEntityFacets({ facets: ['kind'] }) + .then(response => response.facets.kind?.map(f => f.value).sort() || []); }); return { error, loading, kinds }; } diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 548561108d..27fde81cc7 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -29,14 +29,11 @@ import qs from 'qs'; import React, { PropsWithChildren } from 'react'; import { MemoryRouter } from 'react-router'; import { catalogApiRef } from '../api'; -import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; +import { starredEntitiesApiRef, MockStarredEntitiesApi } from '../apis'; import { EntityKindPicker, UserListPicker } from '../components'; import { EntityKindFilter, EntityTypeFilter, UserListFilter } from '../filters'; import { UserListFilterKind } from '../types'; -import { - EntityListProvider, - useEntityListProvider, -} from './useEntityListProvider'; +import { EntityListProvider, useEntityList } from './useEntityListProvider'; const entities: Entity[] = [ { @@ -48,6 +45,7 @@ const entities: Entity[] = [ relations: [ { type: 'ownedBy', + targetRef: 'user:default/guest', target: { name: 'guest', namespace: 'default', @@ -97,12 +95,7 @@ const wrapper = ({ [catalogApiRef, mockCatalogApi], [identityApiRef, mockIdentityApi], [storageApiRef, MockStorageApi.create()], - [ - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), - }), - ], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], ]} > @@ -129,12 +122,9 @@ describe('', () => { }); it('resolves backend filters', async () => { - const { result, waitForValueToChange } = renderHook( - () => useEntityListProvider(), - { - wrapper, - }, - ); + const { result, waitForValueToChange } = renderHook(() => useEntityList(), { + wrapper, + }); await waitForValueToChange(() => result.current.backendEntities); expect(result.current.backendEntities.length).toBe(2); expect(mockCatalogApi.getEntities).toHaveBeenCalledWith({ @@ -143,7 +133,7 @@ describe('', () => { }); it('resolves frontend filters', async () => { - const { result, waitFor } = renderHook(() => useEntityListProvider(), { + const { result, waitFor } = renderHook(() => useEntityList(), { wrapper, initialProps: { userFilter: 'all', @@ -173,7 +163,7 @@ describe('', () => { const query = qs.stringify({ filters: { kind: 'component', type: 'service' }, }); - const { result, waitFor } = renderHook(() => useEntityListProvider(), { + const { result, waitFor } = renderHook(() => useEntityList(), { wrapper, initialProps: { location: `/catalog?${query}`, @@ -187,7 +177,7 @@ describe('', () => { }); it('does not fetch when only frontend filters change', async () => { - const { result, waitFor } = renderHook(() => useEntityListProvider(), { + const { result, waitFor } = renderHook(() => useEntityList(), { wrapper, }); @@ -214,7 +204,7 @@ describe('', () => { it('debounces multiple filter changes', async () => { const { result, waitForNextUpdate, waitForValueToChange } = renderHook( - () => useEntityListProvider(), + () => useEntityList(), { wrapper, }, @@ -233,7 +223,7 @@ describe('', () => { it('returns an error on catalogApi failure', async () => { const { result, waitForValueToChange, waitFor } = renderHook( - () => useEntityListProvider(), + () => useEntityList(), { wrapper, }, diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index 9b4d850208..f448e41b82 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -43,6 +43,7 @@ import { EntityFilter } from '../types'; import { reduceCatalogFilters, reduceEntityFilters } from '../utils'; import { useApi } from '@backstage/core-plugin-api'; +/** @public */ export type DefaultEntityFilters = { kind?: EntityKindFilter; type?: EntityTypeFilter; @@ -53,6 +54,7 @@ export type DefaultEntityFilters = { text?: EntityTextFilter; }; +/** @public */ export type EntityListContextProps< EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, > = { @@ -91,6 +93,10 @@ export type EntityListContextProps< error?: Error; }; +/** + * Creates new context for entity listing and filtering. + * @public + */ export const EntityListContext = createContext< EntityListContextProps | undefined >(undefined); @@ -101,6 +107,10 @@ type OutputState = { backendEntities: Entity[]; }; +/** + * Provides entities and filters for a catalog listing. + * @public + */ export const EntityListProvider = ({ children, }: PropsWithChildren<{}>) => { @@ -239,13 +249,29 @@ export const EntityListProvider = ({ ); }; +/** + * Hook for interacting with the entity list context provided by the {@link EntityListProvider}. + * @public + * @deprecated use {@link useEntityList} instead. + */ export function useEntityListProvider< EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, >(): EntityListContextProps { const context = useContext(EntityListContext); if (!context) - throw new Error( - 'useEntityListProvider must be used within EntityListProvider', - ); + throw new Error('useEntityList must be used within EntityListProvider'); + return context; +} + +/** + * Hook for interacting with the entity list context provided by the {@link EntityListProvider}. + * @public + */ +export function useEntityList< + EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, +>(): EntityListContextProps { + const context = useContext(EntityListContext); + if (!context) + throw new Error('useEntityList must be used within EntityListProvider'); return context; } diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index 7f838163a2..1bb9a649ad 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -26,11 +26,7 @@ import { TestApiProvider } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { catalogApiRef } from '../api'; -import { - loadCatalogOwnerRefs, - loadIdentityOwnerRefs, - useEntityOwnership, -} from './useEntityOwnership'; +import { loadCatalogOwnerRefs, useEntityOwnership } from './useEntityOwnership'; describe('useEntityOwnership', () => { type MockIdentityApi = jest.Mocked>; @@ -70,10 +66,12 @@ describe('useEntityOwnership', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/user1', target: { kind: 'User', namespace: 'default', name: 'user1' }, }, { type: RELATION_OWNED_BY, + targetRef: 'group:default/group1', target: { kind: 'Group', namespace: 'default', name: 'group1' }, }, ], @@ -92,6 +90,7 @@ describe('useEntityOwnership', () => { relations: [ { type: RELATION_MEMBER_OF, + targetRef: 'group:default/group1', target: { kind: 'Group', namespace: 'default', name: 'group1' }, }, ], @@ -101,18 +100,6 @@ describe('useEntityOwnership', () => { jest.resetAllMocks(); }); - describe('loadIdentityOwnerRefs', () => { - it('passes through the ownershipEntityRefs', async () => { - const refs = new Array(); - mockIdentityApi.getBackstageIdentity.mockResolvedValueOnce({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: refs, - }); - await expect(loadIdentityOwnerRefs(identityApi)).resolves.toBe(refs); - }); - }); - describe('loadCatalogOwnerRefs', () => { it('loads the first user from the catalog', async () => { mockCatalogApi.getEntityByName.mockResolvedValueOnce(user2Entity); diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.ts b/plugins/catalog-react/src/hooks/useEntityOwnership.ts index 1138dada1f..de3eca9dab 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.ts +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.ts @@ -17,40 +17,17 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity, - EntityName, parseEntityRef, RELATION_MEMBER_OF, RELATION_OWNED_BY, stringifyEntityRef, } from '@backstage/catalog-model'; -import { - IdentityApi, - identityApiRef, - useApi, -} from '@backstage/core-plugin-api'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; import { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../api'; import { getEntityRelations } from '../utils/getEntityRelations'; -/** - * Takes the relevant parts of the Backstage identity, and translates them into - * a list of entity refs on string form that represent the user's ownership - * connections. - * - * @public - * @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. - * - * @param identityApi - The IdentityApi implementation - * @returns IdentityOwner refs as a string array - */ -export async function loadIdentityOwnerRefs( - identityApi: IdentityApi, -): Promise { - const identity = await identityApi.getBackstageIdentity(); - return identity.ownershipEntityRefs; -} - /** * Takes the relevant parts of the User entity corresponding to the Backstage * identity, and translates them into a list of entity refs on string form that @@ -61,6 +38,7 @@ export async function loadIdentityOwnerRefs( * @param catalogApi - The Catalog API implementation * @param identityOwnerRefs - List of identity owner refs as strings * @returns OwnerRefs as a string array + * @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. */ export async function loadCatalogOwnerRefs( catalogApi: CatalogApi, @@ -98,7 +76,7 @@ export async function loadCatalogOwnerRefs( */ export function useEntityOwnership(): { loading: boolean; - isOwnedEntity: (entity: Entity | EntityName) => boolean; + isOwnedEntity: (entity: Entity) => boolean; } { const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); @@ -115,12 +93,10 @@ export function useEntityOwnership(): { const isOwnedEntity = useMemo(() => { const myOwnerRefs = new Set(refs ?? []); - return (entity: Entity | EntityName) => { - const entityOwnerRefs = ( - 'metadata' in entity - ? getEntityRelations(entity, RELATION_OWNED_BY) - : [entity] - ).map(stringifyEntityRef); + return (entity: Entity) => { + const entityOwnerRefs = getEntityRelations(entity, RELATION_OWNED_BY).map( + stringifyEntityRef, + ); for (const ref of entityOwnerRefs) { if (myOwnerRefs.has(ref)) { return true; diff --git a/plugins/catalog-react/src/hooks/useEntityPermission.ts b/plugins/catalog-react/src/hooks/useEntityPermission.ts index c1f62cfc39..092114936a 100644 --- a/plugins/catalog-react/src/hooks/useEntityPermission.ts +++ b/plugins/catalog-react/src/hooks/useEntityPermission.ts @@ -28,7 +28,7 @@ import { useEntity } from './useEntity'; * Note: this hook blocks the permission request until the entity has loaded in * context. If you have the entityRef and need concurrent requests, use the * `usePermission` hook directly. - * @public + * @alpha */ export function useEntityPermission(permission: Permission): { loading: boolean; diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index c4531d9117..2159c30edc 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -17,12 +17,16 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import isEqual from 'lodash/isEqual'; +import sortBy from 'lodash/sortBy'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '../api'; -import { useEntityListProvider } from './useEntityListProvider'; +import { useEntityList } from './useEntityListProvider'; import { EntityTypeFilter } from '../filters'; -type EntityTypeReturn = { +/** @public + * @deprecated type inlined with {@link useEntityTypeFilter}. + */ +export type EntityTypeReturn = { loading: boolean; error?: Error; availableTypes: string[]; @@ -31,16 +35,23 @@ type EntityTypeReturn = { }; /** - * A hook built on top of `useEntityListProvider` for enabling selection of valid `spec.type` values + * A hook built on top of `useEntityList` for enabling selection of valid `spec.type` values * based on the selected EntityKindFilter. + * @public */ -export function useEntityTypeFilter(): EntityTypeReturn { +export function useEntityTypeFilter(): { + loading: boolean; + error?: Error; + availableTypes: string[]; + selectedTypes: string[]; + setSelectedTypes: (types: string[]) => void; +} { const catalogApi = useApi(catalogApiRef); const { filters: { kind: kindFilter, type: typeFilter }, queryParameters, updateFilters, - } = useEntityListProvider(); + } = useEntityList(); const queryParamTypes = useMemo( () => [queryParameters.type].flat().filter(Boolean) as string[], @@ -67,51 +78,34 @@ export function useEntityTypeFilter(): EntityTypeReturn { const { error, loading, - value: entities, + value: facets, } = useAsync(async () => { if (kind) { const items = await catalogApi - .getEntities({ + .getEntityFacets({ filter: { kind }, - fields: ['spec.type'], + facets: ['spec.type'], }) - .then(response => response.items); + .then(response => response.facets['spec.type'] || []); return items; } return []; }, [kind, catalogApi]); - const entitiesRef = useRef(entities); + const facetsRef = useRef(facets); useEffect(() => { - const oldEntities = entitiesRef.current; - entitiesRef.current = entities; - // Delay processing hook until kind and entity load updates have settled to generate list of types; - // This prevents reseting the type filter due to saved type value from query params not matching the + const oldFacets = facetsRef.current; + facetsRef.current = facets; + // Delay processing hook until kind and facets load updates have settled to generate list of types; + // This prevents resetting the type filter due to saved type value from query params not matching the // empty set of type values while values are still being loaded; also only run this hook on changes - // to entities - if (loading || !kind || oldEntities === entities) { + // to facets + if (loading || !kind || oldFacets === facets || !facets) { return; } - // Resolve the unique set of types from returned entities; could be optimized by a new endpoint - // in the catalog-backend that does this, rather than loading entities with redundant types. - if (!entities) return; - - // Sort by entity count descending, so the most common types appear on top - const countByType = entities.reduce((acc, entity) => { - if (typeof entity.spec?.type !== 'string') return acc; - - const entityType = entity.spec.type.toLocaleLowerCase('en-US'); - if (!acc[entityType]) { - acc[entityType] = 0; - } - acc[entityType] += 1; - return acc; - }, {} as Record); - - const newTypes = Object.entries(countByType) - .sort(([, count1], [, count2]) => count2 - count1) - .map(([type]) => type); + // Sort by facet count descending, so the most common types appear on top + const newTypes = sortBy(facets, f => -f.count).map(f => f.value); setAvailableTypes(newTypes); // Update type filter to only valid values when the list of available types has changed @@ -121,7 +115,7 @@ export function useEntityTypeFilter(): EntityTypeReturn { if (!isEqual(selectedTypes, stillValidTypes)) { setSelectedTypes(stillValidTypes); } - }, [loading, kind, selectedTypes, setSelectedTypes, entities]); + }, [loading, kind, selectedTypes, setSelectedTypes, facets]); useEffect(() => { updateFilters({ diff --git a/plugins/catalog-react/src/hooks/useOwnUser.ts b/plugins/catalog-react/src/hooks/useOwnUser.ts index f49eb768d0..74c44bc638 100644 --- a/plugins/catalog-react/src/hooks/useOwnUser.ts +++ b/plugins/catalog-react/src/hooks/useOwnUser.ts @@ -15,7 +15,7 @@ */ import { - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, parseEntityRef, UserEntity, } from '@backstage/catalog-model'; @@ -25,6 +25,8 @@ import { identityApiRef, useApi } from '@backstage/core-plugin-api'; /** * Get the catalog User entity (if any) that matches the logged-in user. + * @public + * @deprecated due to low external usage. */ export function useOwnUser(): AsyncState { const catalogApi = useApi(catalogApiRef); @@ -35,7 +37,7 @@ export function useOwnUser(): AsyncState { return catalogApi.getEntityByName( parseEntityRef(identity.userEntityRef, { defaultKind: 'User', - defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + defaultNamespace: DEFAULT_NAMESPACE, }), ) as Promise; }, [catalogApi, identityApi]); diff --git a/plugins/catalog-react/src/hooks/useOwnedEntities.ts b/plugins/catalog-react/src/hooks/useOwnedEntities.ts index ffe64830f4..a2258cf138 100644 --- a/plugins/catalog-react/src/hooks/useOwnedEntities.ts +++ b/plugins/catalog-react/src/hooks/useOwnedEntities.ts @@ -14,13 +14,10 @@ * limitations under the License. */ import { catalogApiRef } from './../api'; -import { - loadCatalogOwnerRefs, - loadIdentityOwnerRefs, -} from './useEntityOwnership'; +import { loadCatalogOwnerRefs } 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,17 +29,18 @@ import { useMemo } from 'react'; * @public * * @param allowedKinds - Array of allowed kinds to filter the entities - * @returns CatalogListResponse + * @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. */ export function useOwnedEntities(allowedKinds?: string[]): { loading: boolean; - ownedEntities: CatalogListResponse | undefined; + ownedEntities: GetEntitiesResponse | undefined; } { const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); const { loading, value: refs } = useAsync(async () => { - const identityRefs = await loadIdentityOwnerRefs(identityApi); + const identity = await identityApi.getBackstageIdentity(); + const identityRefs = identity.ownershipEntityRefs; const catalogRefs = await loadCatalogOwnerRefs(catalogApi, identityRefs); const catalogs = await catalogApi.getEntities( allowedKinds diff --git a/plugins/catalog-react/src/hooks/useRelatedEntities.ts b/plugins/catalog-react/src/hooks/useRelatedEntities.ts index 942941e3d5..4cf5690ac3 100644 --- a/plugins/catalog-react/src/hooks/useRelatedEntities.ts +++ b/plugins/catalog-react/src/hooks/useRelatedEntities.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, EntityRelation } from '@backstage/catalog-model'; +import { Entity, parseEntityRef } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { chunk, groupBy } from 'lodash'; import useAsync from 'react-use/lib/useAsync'; @@ -21,30 +21,31 @@ import { catalogApiRef } from '../api'; const BATCH_SIZE = 20; +/** @public */ export function useRelatedEntities( entity: Entity, - { type, kind }: { type?: string; kind?: string }, + relationFilter: { type?: string; kind?: string }, ): { entities: Entity[] | undefined; loading: boolean; error: Error | undefined; } { + const filterByTypeLower = relationFilter?.type?.toLocaleLowerCase('en-US'); + const filterByKindLower = relationFilter?.kind?.toLocaleLowerCase('en-US'); + const catalogApi = useApi(catalogApiRef); const { loading, value: entities, error, } = useAsync(async () => { - const relations = - entity.relations && - entity.relations.filter( + const relations = entity.relations + ?.map(r => ({ type: r.type, target: parseEntityRef(r.targetRef) })) + .filter( r => - (!type || - r.type.toLocaleLowerCase('en-US') === - type.toLocaleLowerCase('en-US')) && - (!kind || - r.target.kind.toLocaleLowerCase('en-US') === - kind.toLocaleLowerCase('en-US')), + (!filterByTypeLower || + r.type.toLocaleLowerCase('en-US') === filterByTypeLower) && + (!filterByKindLower || r.target.kind === filterByKindLower), ); if (!relations) { @@ -56,7 +57,7 @@ export function useRelatedEntities( // `filter=kind=component,namespace=default,name=example1&filter=kind=component,namespace=default,name=example2` // with grouping, we can generate a query a string like // `filter=kind=component,namespace=default,name=example1,example2` - const relationsByKindAndNamespace: EntityRelation[][] = Object.values( + const relationsByKindAndNamespace = Object.values( groupBy(relations, ({ target }) => { return `${target.kind}:${target.namespace}`.toLocaleLowerCase('en-US'); }), @@ -95,7 +96,7 @@ export function useRelatedEntities( ); return results.flatMap(r => r.items); - }, [entity, type]); + }, [entity, filterByTypeLower, filterByKindLower]); return { entities, diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx index aef876a1b2..16fa19a1cd 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx @@ -15,15 +15,18 @@ */ import { Entity } from '@backstage/catalog-model'; -import { StorageApi } from '@backstage/core-plugin-api'; -import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; -import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; +import { + starredEntitiesApiRef, + StarredEntitiesApi, + MockStarredEntitiesApi, +} from '../apis'; import { useStarredEntities } from './useStarredEntities'; describe('useStarredEntities', () => { - let mockStorage: StorageApi; + let mockApi: StarredEntitiesApi; let wrapper: React.ComponentType; const mockEntity: Entity = { @@ -44,22 +47,15 @@ describe('useStarredEntities', () => { }; beforeEach(() => { - mockStorage = MockStorageApi.create(); + mockApi = new MockStarredEntitiesApi(); wrapper = ({ children }: PropsWithChildren<{}>) => ( - + {children} ); }); - it('should return an empty set for when there is no items in storage', async () => { + it('should return an empty set', async () => { const { result, waitForNextUpdate } = renderHook( () => useStarredEntities(), { wrapper }, @@ -70,10 +66,11 @@ describe('useStarredEntities', () => { expect(result.current.starredEntities.size).toBe(0); }); - it('should return a set with the current items when there are items in storage', async () => { + it('should return a set with the current items', async () => { const expectedIds = ['i', 'am', 'some', 'test', 'ids']; - const store = mockStorage?.forBucket('starredEntities'); - await store?.set('entityRefs', expectedIds); + for (const id of expectedIds) { + mockApi.toggleStarred(id); + } const { result, waitForNextUpdate } = renderHook( () => useStarredEntities(), diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.ts b/plugins/catalog-react/src/hooks/useStarredEntities.ts index 066040bf02..5ddabfdd2e 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.ts +++ b/plugins/catalog-react/src/hooks/useStarredEntities.ts @@ -30,6 +30,7 @@ function getEntityRef(entityOrRef: Entity | EntityName | string): string { : stringifyEntityRef(entityOrRef); } +/** @public */ export function useStarredEntities(): { starredEntities: Set; toggleStarredEntity: (entityOrRef: Entity | EntityName | string) => void; diff --git a/plugins/catalog-react/src/hooks/useStarredEntity.ts b/plugins/catalog-react/src/hooks/useStarredEntity.ts index 0233432196..2c647163aa 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntity.ts +++ b/plugins/catalog-react/src/hooks/useStarredEntity.ts @@ -29,6 +29,7 @@ function getEntityRef(entityOrRef: Entity | EntityName | string): string { : stringifyEntityRef(entityOrRef); } +/** @public */ export function useStarredEntity(entityOrRef: Entity | EntityName | string): { toggleStarredEntity: () => void; isStarredEntity: boolean; diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 7bf1bd1221..2279d49a2c 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -27,13 +27,7 @@ export * from './apis'; export * from './components'; export * from './hooks'; export * from './filters'; -export { - catalogRouteRef, - entityRoute, - entityRouteParams, - entityRouteRef, - rootRoute, -} from './routes'; +export { entityRouteParams, entityRouteRef } from './routes'; export * from './testUtils'; export * from './types'; export * from './utils'; diff --git a/plugins/catalog-react/src/routes.ts b/plugins/catalog-react/src/routes.ts index 1fcbd14866..013bf7b5be 100644 --- a/plugins/catalog-react/src/routes.ts +++ b/plugins/catalog-react/src/routes.ts @@ -14,23 +14,10 @@ * limitations under the License. */ -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { createRouteRef } from '@backstage/core-plugin-api'; import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; -// TODO(Rugvip): Move these route refs back to the catalog plugin once we're all ported to using external routes -/** - * @deprecated Use an `ExternalRouteRef` instead, which can point to `catalogPlugin.routes.catalogIndex`. - */ -export const rootRoute = createRouteRef({ - id: 'catalog', -}); - -/** - * @deprecated Use an `ExternalRouteRef` instead, which can point to `catalogPlugin.routes.catalogIndex`. - */ -export const catalogRouteRef = rootRoute; - /** * A stable route ref that points to the catalog page for an individual entity. * @@ -39,6 +26,7 @@ export const catalogRouteRef = rootRoute; * * If you want to replace the `EntityPage` from `@backstage/catalog-plugin` in your app, * you need to use the `entityRouteRef` as the mount point instead of your own. + * @public */ export const entityRouteRef = getOrCreateGlobalSingleton( 'catalog:entity-route-ref', @@ -50,18 +38,15 @@ export const entityRouteRef = getOrCreateGlobalSingleton( ); /** - * @deprecated use `entityRouteRef` instead. + * Utility function to get suitable route params for entityRoute, given an + * @public */ -export const entityRoute = entityRouteRef; - -// Utility function to get suitable route params for entityRoute, given an -// entity instance export function entityRouteParams(entity: Entity) { return { kind: entity.kind.toLocaleLowerCase('en-US'), namespace: entity.metadata.namespace?.toLocaleLowerCase('en-US') ?? - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, name: entity.metadata.name, } as const; } diff --git a/plugins/catalog-react/src/testUtils/providers.tsx b/plugins/catalog-react/src/testUtils/providers.tsx index 3fb295298c..5867026a8c 100644 --- a/plugins/catalog-react/src/testUtils/providers.tsx +++ b/plugins/catalog-react/src/testUtils/providers.tsx @@ -26,6 +26,7 @@ import { EntityListContextProps, } from '../hooks/useEntityListProvider'; +/** @public */ export const MockEntityListContextProvider = ({ children, value, diff --git a/plugins/catalog-react/src/types.ts b/plugins/catalog-react/src/types.ts index 964858cb10..41c7d75f0d 100644 --- a/plugins/catalog-react/src/types.ts +++ b/plugins/catalog-react/src/types.ts @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; +/** @public */ export type EntityFilter = { /** * Get filters to add to the catalog-backend request. These are a dot-delimited field with @@ -37,10 +38,11 @@ export type EntityFilter = { /** * Serialize the filter value to a string for query params. The UI component responsible for - * handling this filter should retrieve this from useEntityListProvider.queryParameters. The + * handling this filter should retrieve this from useEntityList.queryParameters. The * value restored should be in the precedence: queryParameters `>` initialValue prop `>` default. */ toQueryValue?: () => string | string[]; }; +/** @public */ export type UserListFilterKind = 'owned' | 'starred' | 'all'; diff --git a/plugins/catalog-react/src/utils/filters.ts b/plugins/catalog-react/src/utils/filters.ts index 109e864c52..68cd94fd6e 100644 --- a/plugins/catalog-react/src/utils/filters.ts +++ b/plugins/catalog-react/src/utils/filters.ts @@ -17,6 +17,10 @@ import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '../types'; +/** + * @public + * @deprecated will be made private. + */ export function reduceCatalogFilters( filters: EntityFilter[], ): Record { @@ -28,6 +32,10 @@ export function reduceCatalogFilters( }, {} as Record); } +/** + * @public + * @deprecated will be made private. + */ export function reduceEntityFilters( filters: EntityFilter[], ): (entity: Entity) => boolean { diff --git a/plugins/catalog-react/src/utils/getEntityMetadataUrl.ts b/plugins/catalog-react/src/utils/getEntityMetadataUrl.ts index 996f928ecf..6037fc3c4d 100644 --- a/plugins/catalog-react/src/utils/getEntityMetadataUrl.ts +++ b/plugins/catalog-react/src/utils/getEntityMetadataUrl.ts @@ -15,15 +15,22 @@ */ import { - EDIT_URL_ANNOTATION, + ANNOTATION_EDIT_URL, + ANNOTATION_VIEW_URL, Entity, - VIEW_URL_ANNOTATION, } from '@backstage/catalog-model'; +/** + * @public + * @deprecated use entity.metadata.annotations?.[ANNOTATION_VIEW_URL] instead. */ export function getEntityMetadataViewUrl(entity: Entity): string | undefined { - return entity.metadata.annotations?.[VIEW_URL_ANNOTATION]; + return entity.metadata.annotations?.[ANNOTATION_VIEW_URL]; } +/** + * @public + * @deprecated use entity.metadata.annotations?.[ANNOTATION_EDIT_URL] instead. + */ 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/getEntityRelations.test.ts b/plugins/catalog-react/src/utils/getEntityRelations.test.ts index 22c695043f..ea0ba59399 100644 --- a/plugins/catalog-react/src/utils/getEntityRelations.test.ts +++ b/plugins/catalog-react/src/utils/getEntityRelations.test.ts @@ -31,17 +31,19 @@ describe('getEntityRelations', () => { relations: [ { type: RELATION_MEMBER_OF, - target: { kind: 'group', name: 'member' }, + targetRef: 'group:default/member', + target: { kind: 'group', namespace: 'default', name: 'member' }, }, { type: RELATION_CHILD_OF, - target: { kind: 'group', name: 'child' }, + targetRef: 'group:default/child', + target: { kind: 'group', namespace: 'default', name: 'child' }, }, ], } as Entity; expect(getEntityRelations(entity, RELATION_MEMBER_OF)).toEqual([ - { kind: 'group', name: 'member' }, + { kind: 'group', namespace: 'default', name: 'member' }, ]); }); @@ -50,17 +52,19 @@ describe('getEntityRelations', () => { relations: [ { type: RELATION_MEMBER_OF, - target: { kind: 'group', name: 'member' }, + targetRef: 'group:default/member', + target: { kind: 'group', namespace: 'default', name: 'member' }, }, { type: RELATION_MEMBER_OF, - target: { kind: 'user', name: 'child' }, + targetRef: 'user:default/child', + target: { kind: 'user', namespace: 'default', name: 'child' }, }, ], } as Entity; expect( getEntityRelations(entity, RELATION_MEMBER_OF, { kind: 'Group' }), - ).toEqual([{ kind: 'group', name: 'member' }]); + ).toEqual([{ kind: 'group', namespace: 'default', name: 'member' }]); }); }); diff --git a/plugins/catalog-react/src/utils/getEntityRelations.ts b/plugins/catalog-react/src/utils/getEntityRelations.ts index 0045fac4f5..45cbc6967f 100644 --- a/plugins/catalog-react/src/utils/getEntityRelations.ts +++ b/plugins/catalog-react/src/utils/getEntityRelations.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, EntityName, parseEntityRef } from '@backstage/catalog-model'; +// TODO(freben): This should be returning entity refs instead /** * Get the related entity references. + * + * @public */ export function getEntityRelations( entity: Entity | undefined, @@ -27,10 +30,10 @@ export function getEntityRelations( let entityNames = entity?.relations ?.filter(r => r.type === relationType) - ?.map(r => r.target) || []; + .map(r => parseEntityRef(r.targetRef)) || []; if (filter?.kind) { - entityNames = entityNames?.filter( + entityNames = entityNames.filter( e => e.kind.toLocaleLowerCase('en-US') === filter.kind.toLocaleLowerCase('en-US'), diff --git a/plugins/catalog-react/src/utils/getEntitySourceLocation.ts b/plugins/catalog-react/src/utils/getEntitySourceLocation.ts index 59c3b5ab95..d2141449a8 100644 --- a/plugins/catalog-react/src/utils/getEntitySourceLocation.ts +++ b/plugins/catalog-react/src/utils/getEntitySourceLocation.ts @@ -15,30 +15,32 @@ */ import { + ANNOTATION_SOURCE_LOCATION, Entity, - parseLocationReference, - SOURCE_LOCATION_ANNOTATION, + parseLocationRef, } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; +/** @public */ export type EntitySourceLocation = { locationTargetUrl: string; integrationType?: string; }; +/** @public */ export function getEntitySourceLocation( entity: Entity, 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-react/src/utils/isOwnerOf.test.ts b/plugins/catalog-react/src/utils/isOwnerOf.test.ts index 2cd6cfafa3..d3053646d8 100644 --- a/plugins/catalog-react/src/utils/isOwnerOf.test.ts +++ b/plugins/catalog-react/src/utils/isOwnerOf.test.ts @@ -31,6 +31,7 @@ describe('isOwnerOf', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/user', target: { kind: 'user', namespace: 'default', name: 'user' }, }, ], @@ -46,6 +47,7 @@ describe('isOwnerOf', () => { relations: [ { type: RELATION_MEMBER_OF, + targetRef: 'group:default/group', target: { kind: 'group', namespace: 'default', name: 'group' }, }, ], @@ -54,6 +56,7 @@ describe('isOwnerOf', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'group:default/group', target: { kind: 'group', namespace: 'default', name: 'group' }, }, ], diff --git a/plugins/catalog-react/src/utils/isOwnerOf.ts b/plugins/catalog-react/src/utils/isOwnerOf.ts index 38ad90e4a6..3ef6b4b2a0 100644 --- a/plugins/catalog-react/src/utils/isOwnerOf.ts +++ b/plugins/catalog-react/src/utils/isOwnerOf.ts @@ -25,6 +25,7 @@ import { getEntityRelations } from './getEntityRelations'; /** * Get the related entity references. + * @public */ export function isOwnerOf(owner: Entity, owned: Entity) { const possibleOwners = new Set( diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index cb13e27bd0..3c70959b78 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-catalog +## 0.9.0 + +### Minor Changes + +- e7b9c3d713: Removed `columnFactories`, which was accidentally exported on the previous release. +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. +- 3c4376e5e7: **BREAKING**: Removed the old `plugin` export, use `catalogPlugin` instead. + +### Patch Changes + +- 2d339b5f2c: Internalize deprecated `useEntityFromUrl` hook +- a686702dbe: Renamed `CatalogResultListItem` to `CatalogSearchResultListItem` along with its prop type, leaving the old names in place as a deprecations. +- 852d5ff758: Added `EntitySwitchProps` type for `EntitySwitch`. +- a8331830f1: Remove use of deprecated `useEntityKinds` hook. +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- b776ce5aab: Replaced use of deprecated `useEntityListProvider` hook with `useEntityList`. +- 3334ad47d4: Removed usage of `EntityContext`. +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration-react@0.1.23 + +## 0.8.0 + +### Minor Changes + +- ae7edbea45: Removed `CatalogClientWrapper` as usage have been replaced by the `fetchApiRef`. +- 37092662cb: **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`. +- 7657eeae22: Removed deprecated `Router` and `EntityPageLayout` exports. + `Router` is replaced by plugin extensions and `EntityPageLayout` is replaced by `CatalogEntityPage`. +- 0a033351be: **BREAKING**: + + - The `CatalogResultListItem` `result` prop is now of the more narrow and correct type `IndexableDocument`, rather than `any`. + - The individual table column factories (e.g. `createNameColumn`) are now no longer available directly, but only through `CatalogTable.columns`. + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 538ca90790: Use updated type names from `@backstage/catalog-client` +- 0d03e42a89: Replaced use of deprecated `compareEntityToRef` utility. +- 244d24ebc4: Import `Location` from the `@backstage/catalog-client` package. +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- 27eccab216: Replaces use of deprecated catalog-model constants. +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/catalog-client@0.7.0 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/integration-react@0.1.22 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/search-common@0.2.3 + - @backstage/theme@0.2.15 + - @backstage/plugin-catalog-common@0.1.3 + ## 0.7.12 ### Patch Changes diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index fe20c1548b..f314932b9a 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -5,57 +5,59 @@ ```ts /// -import { AddLocationRequest } from '@backstage/catalog-client'; -import { AddLocationResponse } from '@backstage/catalog-client'; 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 { IconComponent } from '@backstage/core-plugin-api'; -import { IdentityApi } from '@backstage/core-plugin-api'; +import { IndexableDocument } from '@backstage/search-common'; import { InfoCardVariants } from '@backstage/core-components'; -import { Location as Location_2 } from '@backstage/catalog-model'; +import { Observable } from '@backstage/types'; import { Overrides } from '@material-ui/core/styles/overrides'; -import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { StarredEntitiesApi } from '@backstage/plugin-catalog-react'; +import { StorageApi } from '@backstage/core-plugin-api'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { TableColumn } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; import { TabProps } from '@material-ui/core'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; -// Warning: (ae-forgotten-export) The symbol "AboutCardProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "AboutCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function AboutCard({ variant }: AboutCardProps): JSX.Element; +// @public @deprecated (undocumented) +export function AboutCard(props: AboutCardProps): 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) "AboutContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const AboutContent: ({ entity }: Props) => JSX.Element; +// @public +export interface AboutCardProps { + // (undocumented) + variant?: InfoCardVariants; +} -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "AboutField" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const AboutField: ({ - label, - value, - gridSizes, - children, -}: Props_2) => JSX.Element; +export function AboutContent(props: AboutContentProps): JSX.Element; + +// @public +export interface AboutContentProps { + // (undocumented) + entity: Entity; +} + +// @public (undocumented) +export function AboutField(props: AboutFieldProps): JSX.Element; + +// @public +export interface AboutFieldProps { + // (undocumented) + children?: React_2.ReactNode; + // (undocumented) + gridSizes?: Record; + // (undocumented) + label: string; + // (undocumented) + value?: string; +} // @public (undocumented) export type BackstageOverrides = Overrides & { @@ -64,85 +66,23 @@ export type BackstageOverrides = Overrides & { >; }; -// Warning: (ae-missing-release-tag) "CatalogClientWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated -export class CatalogClientWrapper implements CatalogApi { - constructor(options: { client: CatalogClient; identityApi: IdentityApi }); - // (undocumented) - addLocation( - request: AddLocationRequest, - options?: CatalogRequestOptions, - ): Promise; - // (undocumented) - getEntities( - request?: CatalogEntitiesRequest, - options?: CatalogRequestOptions, - ): Promise>; - // (undocumented) - getEntityAncestors( - request: CatalogEntityAncestorsRequest, - options?: CatalogRequestOptions, - ): Promise; - // (undocumented) - getEntityByName( - compoundName: EntityName, - options?: CatalogRequestOptions, - ): Promise; - // (undocumented) - getLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise; - // (undocumented) - getLocationById( - id: string, - options?: CatalogRequestOptions, - ): Promise; - // (undocumented) - getOriginLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise; - // (undocumented) - refreshEntity( - entityRef: string, - options?: CatalogRequestOptions, - ): Promise; - // (undocumented) - removeEntityByUid( - uid: string, - options?: CatalogRequestOptions, - ): Promise; - // (undocumented) - removeLocationById( - id: string, - options?: CatalogRequestOptions, - ): Promise; -} - -// Warning: (ae-missing-release-tag) "CatalogEntityPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const CatalogEntityPage: () => JSX.Element; -// Warning: (ae-missing-release-tag) "CatalogIndexPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const CatalogIndexPage: (props: DefaultCatalogPageProps) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "CatalogKindHeaderProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "CatalogKindHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const CatalogKindHeader: ({ - initialFilter, -}: CatalogKindHeaderProps) => JSX.Element; +export function CatalogKindHeader(props: CatalogKindHeaderProps): JSX.Element; + +// @public +export interface CatalogKindHeaderProps { + // (undocumented) + initialFilter?: string; +} -// Warning: (ae-missing-release-tag) "catalogPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -const catalogPlugin: BackstagePlugin< +export const catalogPlugin: BackstagePlugin< { catalogIndex: RouteRef; catalogEntity: RouteRef<{ @@ -163,27 +103,57 @@ const catalogPlugin: BackstagePlugin< >; } >; -export { catalogPlugin }; -export { catalogPlugin as plugin }; -// Warning: (ae-missing-release-tag) "CatalogResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public @deprecated (undocumented) +export const CatalogResultListItem: typeof CatalogSearchResultListItem; + +// @public @deprecated (undocumented) +export type CatalogResultListItemProps = CatalogSearchResultListItemProps; + // @public (undocumented) -export const CatalogResultListItem: ({ result }: any) => JSX.Element; +export function CatalogSearchResultListItem( + props: CatalogSearchResultListItemProps, +): JSX.Element; + +// @public +export interface CatalogSearchResultListItemProps { + // (undocumented) + result: IndexableDocument; +} -// Warning: (ae-missing-release-tag) "CatalogTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const CatalogTable: { - ({ columns, actions }: CatalogTableProps): JSX.Element; - columns: typeof columnFactories; + (props: CatalogTableProps): JSX.Element; + columns: Readonly<{ + createNameColumn( + options?: + | { + defaultKind?: string | undefined; + } + | undefined, + ): TableColumn; + createSystemColumn(): TableColumn; + createOwnerColumn(): TableColumn; + createSpecTypeColumn(): TableColumn; + createSpecLifecycleColumn(): TableColumn; + createMetadataDescriptionColumn(): TableColumn; + createTagsColumn(): TableColumn; + }>; }; -// Warning: (ae-missing-release-tag) "EntityRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export interface CatalogTableProps { + // (undocumented) + actions?: TableProps['actions']; + // (undocumented) + columns?: TableColumn[]; +} + // @public (undocumented) -export type CatalogTableRow = { +export interface CatalogTableRow { + // (undocumented) entity: Entity; + // (undocumented) resolved: { name: string; partOfSystemRelationTitle?: string; @@ -191,246 +161,226 @@ export type CatalogTableRow = { ownedByRelationsTitle?: string; ownedByRelations: EntityName[]; }; -}; - -// 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) -export function createMetadataDescriptionColumn(): TableColumn; - -// Warning: (ae-forgotten-export) The symbol "NameColumnProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createNameColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function createNameColumn( - props?: NameColumnProps, -): 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) -export 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) -export 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) -export 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) -export function createSystemColumn(): TableColumn; - -// Warning: (ae-missing-release-tag) "createTagsColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function createTagsColumn(): TableColumn; +} // @public -export type DefaultCatalogPageProps = { - initiallySelectedFilter?: UserListFilterKind; - columns?: TableColumn[]; +export interface DefaultCatalogPageProps { + // (undocumented) actions?: TableProps['actions']; -}; + // (undocumented) + columns?: TableColumn[]; + // (undocumented) + initiallySelectedFilter?: UserListFilterKind; +} + +// @public +export class DefaultStarredEntitiesApi implements StarredEntitiesApi { + constructor(opts: { storageApi: StorageApi }); + // (undocumented) + starredEntitie$(): Observable>; + // (undocumented) + toggleStarred(entityRef: string): Promise; +} -// Warning: (ae-missing-release-tag) "EntityAboutCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityAboutCard: AboutCard; +export interface DependencyOfComponentsCardProps { + // (undocumented) + title?: string; + // (undocumented) + variant?: 'gridItem'; +} -// Warning: (ae-missing-release-tag) "EntityDependencyOfComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityDependencyOfComponentsCard: ({ - variant, - title, -}: { - variant?: 'gridItem' | undefined; - title?: string | undefined; -}) => JSX.Element; +export interface DependsOnComponentsCardProps { + // (undocumented) + title?: string; + // (undocumented) + variant?: 'gridItem'; +} -// Warning: (ae-missing-release-tag) "EntityDependsOnComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityDependsOnComponentsCard: ({ - variant, - title, -}: { - variant?: 'gridItem' | undefined; - title?: string | undefined; -}) => JSX.Element; +export interface DependsOnResourcesCardProps { + // (undocumented) + variant?: 'gridItem'; +} -// Warning: (ae-missing-release-tag) "EntityDependsOnResourcesCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityDependsOnResourcesCard: ({ - variant, -}: { - variant?: 'gridItem' | undefined; -}) => JSX.Element; +export const EntityAboutCard: (props: AboutCardProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "EntityHasComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityHasComponentsCard: ({ - variant, -}: { - variant?: 'gridItem' | undefined; -}) => JSX.Element; +export const EntityDependencyOfComponentsCard: ( + props: DependencyOfComponentsCardProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "EntityHasResourcesCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityHasResourcesCard: ({ - variant, -}: { - variant?: 'gridItem' | undefined; -}) => JSX.Element; +export const EntityDependsOnComponentsCard: ( + props: DependsOnComponentsCardProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "EntityHasSubcomponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityHasSubcomponentsCard: ({ - variant, -}: { - variant?: 'gridItem' | undefined; -}) => JSX.Element; +export const EntityDependsOnResourcesCard: ( + props: DependsOnResourcesCardProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "EntityHasSystemsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityHasSystemsCard: ({ - variant, -}: { - variant?: 'gridItem' | undefined; -}) => JSX.Element; +export const EntityHasComponentsCard: ( + props: HasComponentsCardProps, +) => JSX.Element; + +// @public (undocumented) +export const EntityHasResourcesCard: ( + props: HasResourcesCardProps, +) => JSX.Element; + +// @public (undocumented) +export const EntityHasSubcomponentsCard: ( + props: HasSubcomponentsCardProps, +) => JSX.Element; + +// @public (undocumented) +export const EntityHasSystemsCard: (props: HasSystemsCardProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "EntityLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const EntityLayout: { - ({ - UNSTABLE_extraContextMenuItems, - UNSTABLE_contextMenuOptions, - children, - }: EntityLayoutProps): JSX.Element; - Route: (props: SubRoute) => null; + (props: EntityLayoutProps): JSX.Element; + Route: (props: EntityLayoutRouteProps) => null; }; -// Warning: (ae-missing-release-tag) "EntityLinksCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// @public (undocumented) +export interface EntityLayoutProps { + // (undocumented) + children?: React_2.ReactNode; + // Warning: (ae-forgotten-export) The symbol "contextMenuOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + UNSTABLE_contextMenuOptions?: contextMenuOptions; + // Warning: (ae-forgotten-export) The symbol "ExtraContextMenuItem" needs to be exported by the entry point index.d.ts + // + // (undocumented) + UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; +} + +// @public (undocumented) +export type EntityLayoutRouteProps = { + path: string; + title: string; + children: JSX.Element; + if?: (entity: Entity) => boolean; + tabProps?: TabProps< + React_2.ElementType, + { + component?: React_2.ElementType; + } + >; +}; + +// Warning: (ae-forgotten-export) The symbol "EntityLinksCard" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const EntityLinksCard: ({ - cols, - variant, -}: { - entity?: Entity | undefined; - cols?: number | ColumnBreakpoints | undefined; - variant?: 'gridItem' | undefined; -}) => JSX.Element; +export const EntityLinksCard: EntityLinksCard_2; + +// @public (undocumented) +export interface EntityLinksCardProps { + // Warning: (ae-forgotten-export) The symbol "ColumnBreakpoints" needs to be exported by the entry point index.d.ts + // + // (undocumented) + cols?: ColumnBreakpoints | number; + // (undocumented) + variant?: 'gridItem'; +} // @public (undocumented) export type EntityLinksEmptyStateClassKey = 'code'; -// Warning: (ae-missing-release-tag) "EntityListContainer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityListContainer: ({ - children, -}: PropsWithChildren<{}>) => JSX.Element; +export function EntityListContainer(props: { + children: React_2.ReactNode; +}): JSX.Element; -// Warning: (ae-missing-release-tag) "EntityOrphanWarning" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const EntityOrphanWarning: () => JSX.Element; +export function 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 @deprecated -export const EntityPageLayout: { - ({ - children, - UNSTABLE_extraContextMenuItems, - UNSTABLE_contextMenuOptions, - }: EntityPageLayoutProps): JSX.Element; - Content: (_props: { - path: string; - title: string; - element: JSX.Element; - }) => null; -}; - -// Warning: (ae-missing-release-tag) "EntityProcessingErrorsPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const EntityProcessingErrorsPanel: () => JSX.Element | null; +export function EntityProcessingErrorsPanel(): JSX.Element | null; -// Warning: (ae-missing-release-tag) "EntitySwitch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntitySwitch: { - ({ children }: PropsWithChildren<{}>): JSX.Element | null; - Case: (_: { - if?: - | (( - entity: Entity, - context: { - apis: ApiHolder; - }, - ) => boolean | Promise) - | undefined; - children: ReactNode; - }) => null; + (props: EntitySwitchProps): JSX.Element | null; + Case: (_props: EntitySwitchCaseProps) => null; }; -// 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) -export const FilterContainer: ({ - children, -}: PropsWithChildren<{}>) => JSX.Element; +export interface EntitySwitchCaseProps { + // (undocumented) + children: ReactNode; + // (undocumented) + if?: ( + entity: Entity, + context: { + apis: ApiHolder; + }, + ) => boolean | Promise; +} -// Warning: (ae-missing-release-tag) "FilteredEntityLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const FilteredEntityLayout: ({ - children, -}: PropsWithChildren<{}>) => JSX.Element; +// @public +export interface EntitySwitchProps { + // (undocumented) + children: ReactNode; +} -// Warning: (ae-missing-release-tag) "hasCatalogProcessingErrors" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const hasCatalogProcessingErrors: ( +export function FilterContainer(props: { + children: React_2.ReactNode; +}): JSX.Element; + +// @public (undocumented) +export function FilteredEntityLayout(props: { + children: React_2.ReactNode; +}): JSX.Element; + +// @public +export function hasCatalogProcessingErrors( entity: Entity, context: { apis: ApiHolder; }, -) => Promise; +): Promise; -// Warning: (ae-missing-release-tag) "isComponentType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) +export interface HasComponentsCardProps { + // (undocumented) + variant?: 'gridItem'; +} + +// @public (undocumented) +export interface HasResourcesCardProps { + // (undocumented) + variant?: 'gridItem'; +} + +// @public (undocumented) +export interface HasSubcomponentsCardProps { + // (undocumented) + variant?: 'gridItem'; +} + +// @public (undocumented) +export interface HasSystemsCardProps { + // (undocumented) + variant?: 'gridItem'; +} + +// @public export function isComponentType(type: string): (entity: Entity) => boolean; -// Warning: (ae-missing-release-tag) "isKind" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function isKind(kind: string): (entity: Entity) => boolean; -// Warning: (ae-missing-release-tag) "isNamespace" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function isNamespace(namespace: string): (entity: Entity) => boolean; -// Warning: (ae-missing-release-tag) "isOrphan" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const isOrphan: (entity: Entity) => boolean; +// @public +export function isOrphan(entity: Entity): boolean; // @public (undocumented) export type PluginCatalogComponentsNameToClassKey = { @@ -438,28 +388,22 @@ export type PluginCatalogComponentsNameToClassKey = { PluginCatalogSystemDiagramCard: SystemDiagramCardClassKey; }; -// Warning: (ae-missing-release-tag) "RelatedEntitiesCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const RelatedEntitiesCard: (props: { - variant?: 'gridItem' | undefined; +export const RelatedEntitiesCard: ( + props: RelatedEntitiesCardProps, +) => JSX.Element; + +// @public (undocumented) +export type RelatedEntitiesCardProps = { + variant?: 'gridItem'; title: string; columns: TableColumn[]; - entityKind?: string | undefined; + entityKind?: string; relationType: string; emptyMessage: string; emptyHelpLink: string; asRenderableEntities: (entities: Entity[]) => T[]; -}) => JSX.Element; - -// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const Router: ({ - EntityPage, -}: { - EntityPage?: React_2.ComponentType<{}> | undefined; -}) => JSX.Element; +}; // @public (undocumented) export type SystemDiagramCardClassKey = @@ -468,13 +412,4 @@ export type SystemDiagramCardClassKey = | 'componentNode' | 'apiNode' | 'resourceNode'; - -// Warnings were encountered during analysis: -// -// src/components/CatalogTable/CatalogTable.d.ts:10:5 - (ae-forgotten-export) The symbol "CatalogTableProps" needs to be exported by the entry point index.d.ts -// 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: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 a2492aaba7..6179cf6c44 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.12", + "version": "0.9.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,25 +24,27 @@ "backstage" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.6.0", - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@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", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/integration-react": "^0.1.23", + "@backstage/plugin-catalog-common": "^0.1.4", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/search-common": "^0.2.4", + "@backstage/theme": "^0.2.15", + "@backstage/types": "^0.1.2", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -47,18 +52,19 @@ "lodash": "^4.17.21", "react-helmet": "6.1.0", "react-router": "6.0.0-beta.0", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "zen-observable": "^0.8.15" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@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.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/test-utils": "^0.2.6", "@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/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts deleted file mode 100644 index e1bba9e60f..0000000000 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ /dev/null @@ -1,153 +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 { CatalogClient } from '@backstage/catalog-client'; -import { CatalogClientWrapper } from './CatalogClientWrapper'; -import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; - -jest.mock('@backstage/catalog-client'); -const MockedCatalogClient = CatalogClient as jest.Mock; - -const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; -const discoveryApi: DiscoveryApi = { - async getBaseUrl(_pluginId) { - return mockBaseUrl; - }, -}; -const identityApi: IdentityApi = { - signOut: jest.fn(), - getProfileInfo: jest.fn(), - getBackstageIdentity: jest.fn(), - getCredentials: jest.fn().mockResolvedValue({ token: 'fake-id-token' }), -}; -const guestIdentityApi: IdentityApi = { - signOut: jest.fn(), - getProfileInfo: jest.fn(), - getBackstageIdentity: jest.fn(), - getCredentials: jest.fn().mockResolvedValue({ token: undefined }), -}; - -describe('CatalogClientWrapper', () => { - beforeEach(() => { - MockedCatalogClient.mockClear(); - }); - - describe('getEntities', () => { - it('injects authorization token', async () => { - const client = new CatalogClientWrapper({ - client: new MockedCatalogClient({ discoveryApi }), - identityApi, - }); - await client.getEntities(); - const getEntities = MockedCatalogClient.mock.instances[0].getEntities; - expect(getEntities).toHaveBeenCalledWith(undefined, { - token: 'fake-id-token', - }); - expect(getEntities).toHaveBeenCalledTimes(1); - }); - }); - - describe('getLocationById', () => { - it('omits authorization token when guest', async () => { - const client = new CatalogClientWrapper({ - client: new MockedCatalogClient({ discoveryApi }), - identityApi: guestIdentityApi, - }); - await client.getLocationById('42'); - const getLocationById = - MockedCatalogClient.mock.instances[0].getLocationById; - expect(getLocationById).toHaveBeenCalledWith('42', {}); - expect(getLocationById).toHaveBeenCalledTimes(1); - }); - }); - - describe('getEntityByName', () => { - const name = { - kind: 'kind', - namespace: 'namespace', - name: 'name', - }; - it('injects authorization token', async () => { - const client = new CatalogClientWrapper({ - client: new MockedCatalogClient({ discoveryApi }), - identityApi, - }); - await client.getEntityByName(name); - const getEntityByName = - MockedCatalogClient.mock.instances[0].getEntityByName; - expect(getEntityByName).toHaveBeenCalledWith(name, { - token: 'fake-id-token', - }); - expect(getEntityByName).toHaveBeenCalledTimes(1); - }); - }); - - describe('addLocation', () => { - const location = { target: 'target' }; - it('injects authorization token', async () => { - const client = new CatalogClientWrapper({ - client: new MockedCatalogClient({ discoveryApi }), - identityApi, - }); - await client.addLocation(location); - const addLocation = MockedCatalogClient.mock.instances[0].addLocation; - expect(addLocation).toHaveBeenCalledWith(location, { - token: 'fake-id-token', - }); - expect(addLocation).toHaveBeenCalledTimes(1); - }); - }); - - describe('getLocationByEntity', () => { - const entity = { - apiVersion: 'apiVersion', - kind: 'kind', - metadata: { - name: 'name', - }, - }; - it('injects authorization token', async () => { - const client = new CatalogClientWrapper({ - client: new MockedCatalogClient({ discoveryApi }), - identityApi, - }); - await client.getLocationByEntity(entity); - const getLocationByEntity = - MockedCatalogClient.mock.instances[0].getLocationByEntity; - expect(getLocationByEntity).toHaveBeenCalledWith(entity, { - token: 'fake-id-token', - }); - expect(getLocationByEntity).toHaveBeenCalledTimes(1); - }); - }); - - describe('removeEntityByUid', () => { - it('injects authorization token', async () => { - const uid = 'uid'; - const client = new CatalogClientWrapper({ - client: new MockedCatalogClient({ discoveryApi }), - identityApi, - }); - await client.removeEntityByUid(uid); - const removeEntityByUid = - MockedCatalogClient.mock.instances[0].removeEntityByUid; - expect(removeEntityByUid).toHaveBeenCalledWith(uid, { - token: 'fake-id-token', - }); - expect(removeEntityByUid).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts deleted file mode 100644 index 939c4f095b..0000000000 --- a/plugins/catalog/src/CatalogClientWrapper.ts +++ /dev/null @@ -1,158 +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 { Entity, EntityName, Location } from '@backstage/catalog-model'; -import { - AddLocationRequest, - AddLocationResponse, - CatalogApi, - CatalogClient, - CatalogEntitiesRequest, - CatalogListResponse, - CatalogRequestOptions, - CatalogEntityAncestorsRequest, - CatalogEntityAncestorsResponse, -} from '@backstage/catalog-client'; -import { IdentityApi } from '@backstage/core-plugin-api'; - -/** - * CatalogClient wrapper that injects identity token for all requests - * - * @deprecated The default catalog client now uses the `fetchApiRef` - * implementation, which in turn by default issues tokens just the same as this - * class used to assist in doing. If you use a custom `fetchApiRef` - * implementation that does NOT issue tokens, or use a custom `catalogApiRef` - * implementation which does not use the default `fetchApiRef`, you can wrap - * your catalog API in this class to get back the old behavior. - */ -export class CatalogClientWrapper implements CatalogApi { - private readonly identityApi: IdentityApi; - private readonly client: CatalogClient; - - constructor(options: { client: CatalogClient; identityApi: IdentityApi }) { - this.client = options.client; - this.identityApi = options.identityApi; - } - - async getLocationById( - id: string, - options?: CatalogRequestOptions, - ): Promise { - return await this.client.getLocationById( - id, - await this.getCredentials(options), - ); - } - - async getEntities( - request?: CatalogEntitiesRequest, - options?: CatalogRequestOptions, - ): Promise> { - return await this.client.getEntities( - request, - await this.getCredentials(options), - ); - } - - async getEntityByName( - compoundName: EntityName, - options?: CatalogRequestOptions, - ): Promise { - return await this.client.getEntityByName( - compoundName, - await this.getCredentials(options), - ); - } - - async addLocation( - request: AddLocationRequest, - options?: CatalogRequestOptions, - ): Promise { - return await this.client.addLocation( - request, - await this.getCredentials(options), - ); - } - - async getOriginLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise { - return await this.client.getOriginLocationByEntity( - entity, - await this.getCredentials(options), - ); - } - - async getLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise { - return await this.client.getLocationByEntity( - entity, - await this.getCredentials(options), - ); - } - - async removeLocationById( - id: string, - options?: CatalogRequestOptions, - ): Promise { - return await this.client.removeLocationById( - id, - await this.getCredentials(options), - ); - } - - async removeEntityByUid( - uid: string, - options?: CatalogRequestOptions, - ): Promise { - return await this.client.removeEntityByUid( - uid, - await this.getCredentials(options), - ); - } - - async refreshEntity( - entityRef: string, - options?: CatalogRequestOptions, - ): Promise { - return await this.client.refreshEntity( - entityRef, - await this.getCredentials(options), - ); - } - - async getEntityAncestors( - request: CatalogEntityAncestorsRequest, - options?: CatalogRequestOptions, - ): Promise { - return await this.client.getEntityAncestors( - request, - await this.getCredentials(options), - ); - } - - private async getCredentials( - options?: CatalogRequestOptions, - ): Promise<{ token?: string }> { - if (options?.token) { - return { token: options?.token }; - } - return this.identityApi.getCredentials(); - } -} diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts new file mode 100644 index 0000000000..2a3c4d83ed --- /dev/null +++ b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts @@ -0,0 +1,95 @@ +/* + * 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 { MockStorageApi } from '@backstage/test-utils'; +import { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; +import { performMigrationToTheNewBucket } from './migration'; + +jest.mock('./migration'); + +function getStarred(api: DefaultStarredEntitiesApi) { + return new Promise((resolve, reject) => { + const subscription = api.starredEntitie$().subscribe({ + next(starred) { + resolve(starred); + subscription.unsubscribe(); + }, + error: reject, + }); + }); +} + +describe('DefaultStarredEntitiesApi', () => { + beforeEach(() => { + (performMigrationToTheNewBucket as jest.Mock).mockResolvedValue(undefined); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('constructor', () => { + it('should call migration', () => { + const api = new DefaultStarredEntitiesApi({ + storageApi: MockStorageApi.create(), + }); + expect(performMigrationToTheNewBucket).toBeCalledTimes(1); + expect(api).toBeDefined(); + }); + }); + + it('should notify and toggle starred entities', async () => { + const entityRef = 'component:default/mock'; + + const storageApi = MockStorageApi.create(); + const storageBucket = storageApi.forBucket('starredEntities'); + const api = new DefaultStarredEntitiesApi({ storageApi }); + + const values = new Array>(); + api.starredEntitie$().subscribe({ + next: value => { + values.push(value); + }, + }); + + await expect(getStarred(api)).resolves.toEqual(new Set()); + + await api.toggleStarred(entityRef); + await expect(getStarred(api)).resolves.toEqual(new Set([entityRef])); + expect(storageBucket.snapshot('entityRefs')).toEqual( + expect.objectContaining({ presence: 'present', value: [entityRef] }), + ); + + await api.toggleStarred(entityRef); + await expect(getStarred(api)).resolves.toEqual(new Set()); + expect(storageBucket.snapshot('entityRefs')).toEqual( + expect.objectContaining({ presence: 'present', value: [] }), + ); + + expect(values).toEqual([new Set(), new Set([entityRef]), new Set()]); + }); + + it('should read starred entities from storage', async () => { + const entityRef = 'component:default/mock'; + + const storageApi = MockStorageApi.create(); + const storageBucket = storageApi.forBucket('starredEntities'); + storageBucket.set('entityRefs', [entityRef]); + const api = new DefaultStarredEntitiesApi({ storageApi }); + + await expect(getStarred(api)).resolves.toEqual(new Set([entityRef])); + }); +}); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts similarity index 86% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts index 46a8e63084..f4b7771d04 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts @@ -15,10 +15,10 @@ */ import { StorageApi } from '@backstage/core-plugin-api'; +import { StarredEntitiesApi } from '@backstage/plugin-catalog-react'; import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; import { performMigrationToTheNewBucket } from './migration'; -import { StarredEntitiesApi } from './StarredEntitiesApi'; /** * Default implementation of the StarredEntitiesApi that is backed by the StorageApi. @@ -36,12 +36,12 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { this.settingsStore = opts.storageApi.forBucket('starredEntities'); this.starredEntities = new Set( - this.settingsStore.get('entityRefs') ?? [], + this.settingsStore.snapshot('entityRefs').value ?? [], ); this.settingsStore.observe$('entityRefs').subscribe({ next: next => { - this.starredEntities = new Set(next.newValue ?? []); + this.starredEntities = new Set(next.value ?? []); this.notifyChanges(); }, }); @@ -64,17 +64,13 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { return this.observable; } - isStarred(entityRef: string): boolean { - return this.starredEntities.has(entityRef); - } - private readonly subscribers = new Set< ZenObservable.SubscriptionObserver> >(); private readonly observable = new ObservableImpl>(subscriber => { // forward the the latest value - subscriber.next(this.starredEntities); + subscriber.next(new Set(this.starredEntities)); this.subscribers.add(subscriber); return () => { @@ -84,7 +80,7 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { private notifyChanges() { for (const subscription of this.subscribers) { - subscription.next(this.starredEntities); + subscription.next(new Set(this.starredEntities)); } } } diff --git a/plugins/catalog-backend/src/ingestion/providers/index.ts b/plugins/catalog/src/apis/StarredEntitiesApi/index.ts similarity index 89% rename from plugins/catalog-backend/src/ingestion/providers/index.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/index.ts index cd1bc5cb36..42dc977fb6 100644 --- a/plugins/catalog-backend/src/ingestion/providers/index.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { GitHubOrgEntityProvider } from './GitHubOrgEntityProvider'; +export { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts similarity index 81% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/migration.test.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts index 7eb5648d13..b3bc5400d5 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.test.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts @@ -41,19 +41,19 @@ describe('performMigrationToTheNewBucket', () => { 'entity:Component:default:a', 'entity:template:custom:b', ]); - expect(oldBucket.get('starredEntities')).not.toBeUndefined(); + expect(oldBucket.snapshot('starredEntities').value).not.toBeUndefined(); await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(await newBucket.get('entityRefs')).toEqual([ + expect(await newBucket.snapshot('entityRefs').value).toEqual([ 'component:default/c', 'component:default/a', 'template:custom/b', ]); // OLD bucket should be removed - expect(oldBucket.get('starredEntities')).toBeUndefined(); + expect(oldBucket.snapshot('starredEntities').value).toBeUndefined(); }); it('should ignore invalid entries', async () => { @@ -67,15 +67,17 @@ describe('performMigrationToTheNewBucket', () => { 'entity:Component:a', 'invalid', ]); - expect(oldBucket.get('starredEntities')).not.toBeUndefined(); + expect(oldBucket.snapshot('starredEntities')).not.toBeUndefined(); await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(await newBucket.get('entityRefs')).toEqual(['component:default/a']); + expect(await newBucket.snapshot('entityRefs').value).toEqual([ + 'component:default/a', + ]); // OLD bucket should be removed - expect(oldBucket.get('starredEntities')).toBeUndefined(); + expect(oldBucket.snapshot('starredEntities').value).toBeUndefined(); }); it('should skip migration without old starred entities', async () => { @@ -88,7 +90,7 @@ describe('performMigrationToTheNewBucket', () => { await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(newBucket.get('entityRefs')).toEqual(expectedEntries); + expect(newBucket.snapshot('entityRefs').value).toEqual(expectedEntries); }); it('should skip migration with non-array old starred entities', async () => { @@ -105,9 +107,9 @@ describe('performMigrationToTheNewBucket', () => { await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(newBucket.get('entityRefs')).toEqual(expectedEntries); + expect(newBucket.snapshot('entityRefs').value).toEqual(expectedEntries); // OLD bucket should be unchanged - expect(oldBucket.get('starredEntities')).toBe('invalid'); + expect(oldBucket.snapshot('starredEntities').value).toBe('invalid'); }); }); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.ts similarity index 92% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/migration.ts index 8405d9e08d..51dcd6e7ce 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.ts @@ -35,13 +35,15 @@ export async function performMigrationToTheNewBucket({ const source = storageApi.forBucket('settings'); const target = storageApi.forBucket('starredEntities'); - const oldStarredEntities = source.get('starredEntities'); + const oldStarredEntities = source.snapshot('starredEntities').value; if (!isArray(oldStarredEntities)) { // nothing to do return; } - const targetEntities = new Set(target.get('entityRefs') ?? []); + const targetEntities = new Set( + target.snapshot('entityRefs').value ?? [], + ); oldStarredEntities .filter(isString) diff --git a/plugins/scaffolder/src/components/fields/TextValuePicker/index.ts b/plugins/catalog/src/apis/index.ts similarity index 91% rename from plugins/scaffolder/src/components/fields/TextValuePicker/index.ts rename to plugins/catalog/src/apis/index.ts index 8ab810b6ed..5c7e980890 100644 --- a/plugins/scaffolder/src/components/fields/TextValuePicker/index.ts +++ b/plugins/catalog/src/apis/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { TextValuePicker } from './TextValuePicker'; + +export * from './StarredEntitiesApi'; diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 4ef97b3d46..9a878e3b60 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -38,7 +38,7 @@ describe('', () => { getEntityByName: jest.fn(), getEntities: jest.fn(), addLocation: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), } as any; @@ -63,6 +63,7 @@ describe('', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/guest', target: { kind: 'user', name: 'guest', diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index a6c9635db2..55cef5a5bd 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -15,9 +15,9 @@ */ import { - Entity, - ENTITY_DEFAULT_NAMESPACE, - LOCATION_ANNOTATION, + ANNOTATION_EDIT_URL, + ANNOTATION_LOCATION, + DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; import { @@ -33,7 +33,6 @@ import { } from '@backstage/integration-react'; import { catalogApiRef, - getEntityMetadataEditUrl, getEntitySourceLocation, useEntity, } from '@backstage/plugin-catalog-react'; @@ -72,13 +71,21 @@ const useStyles = makeStyles({ }, }); -type AboutCardProps = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; +/** + * Props for {@link AboutCard}. + * + * @public + */ +export interface AboutCardProps { variant?: InfoCardVariants; -}; +} -export function AboutCard({ variant }: AboutCardProps) { +/** + * @public + * @deprecated Please use EntityAboutCard instead + */ +export function AboutCard(props: AboutCardProps) { + const { variant } = props; const classes = useStyles(); const { entity } = useEntity(); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); @@ -90,7 +97,8 @@ export function AboutCard({ variant }: AboutCardProps) { entity, scmIntegrationsApi, ); - const entityMetadataEditUrl = getEntityMetadataEditUrl(entity); + const entityMetadataEditUrl = + entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; const viewInSource: IconLinkVerticalProps = { label: 'View Source', @@ -107,7 +115,7 @@ export function AboutCard({ variant }: AboutCardProps) { href: viewTechdocLink && viewTechdocLink({ - namespace: entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE, + namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, kind: entity.kind, name: entity.metadata.name, }), @@ -127,7 +135,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/AboutCard/AboutContent.test.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.test.tsx index 860ef2c532..cf9673eeeb 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.test.tsx @@ -47,6 +47,7 @@ describe('', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/o', target: { kind: 'user', name: 'o', @@ -55,6 +56,7 @@ describe('', () => { }, { type: RELATION_PART_OF, + targetRef: 'system:default/s', target: { kind: 'system', name: 's', @@ -63,6 +65,7 @@ describe('', () => { }, { type: RELATION_PART_OF, + targetRef: 'domain:default/d', target: { kind: 'domain', name: 'd', @@ -152,6 +155,7 @@ describe('', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/guest', target: { kind: 'user', name: 'guest', @@ -160,6 +164,7 @@ describe('', () => { }, { type: RELATION_PART_OF, + targetRef: 'system:default/system', target: { kind: 'system', name: 'system', @@ -257,6 +262,7 @@ describe('', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/guest', target: { kind: 'user', name: 'guest', @@ -265,6 +271,7 @@ describe('', () => { }, { type: RELATION_PART_OF, + targetRef: 'system:default/system', target: { kind: 'system', name: 'system', @@ -273,6 +280,7 @@ describe('', () => { }, { type: RELATION_PART_OF, + targetRef: 'component:default/parent-software', target: { kind: 'component', name: 'parent-software', @@ -369,6 +377,7 @@ describe('', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/guest', target: { kind: 'user', name: 'guest', @@ -528,6 +537,7 @@ describe('', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/guest', target: { kind: 'user', name: 'guest', @@ -536,6 +546,7 @@ describe('', () => { }, { type: RELATION_PART_OF, + targetRef: 'system:default/system', target: { kind: 'system', name: 'system', @@ -625,6 +636,7 @@ describe('', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'user:default/guest', target: { kind: 'user', name: 'guest', @@ -633,6 +645,7 @@ describe('', () => { }, { type: RELATION_PART_OF, + targetRef: 'domain:default/domain', target: { kind: 'domain', name: 'domain', diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index 9c8260c14d..ee1dad5d39 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -33,11 +33,18 @@ const useStyles = makeStyles({ }, }); -type Props = { +/** + * Props for {@link AboutContent}. + * + * @public + */ +export interface AboutContentProps { entity: Entity; -}; +} -export const AboutContent = ({ entity }: Props) => { +/** @public */ +export function AboutContent(props: AboutContentProps) { + const { entity } = props; const classes = useStyles(); const isSystem = entity.kind.toLocaleLowerCase('en-US') === 'system'; const isResource = entity.kind.toLocaleLowerCase('en-US') === 'resource'; @@ -154,4 +161,4 @@ export const AboutContent = ({ entity }: Props) => { ); -}; +} diff --git a/plugins/catalog/src/components/AboutCard/AboutField.tsx b/plugins/catalog/src/components/AboutCard/AboutField.tsx index c4dd107434..6ceaa4fd7a 100644 --- a/plugins/catalog/src/components/AboutCard/AboutField.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutField.tsx @@ -36,14 +36,21 @@ const useStyles = makeStyles(theme => ({ }, })); -type Props = { +/** + * Props for {@link AboutField}. + * + * @public + */ +export interface AboutFieldProps { label: string; value?: string; gridSizes?: Record; children?: React.ReactNode; -}; +} -export const AboutField = ({ label, value, gridSizes, children }: Props) => { +/** @public */ +export function AboutField(props: AboutFieldProps) { + const { label, value, gridSizes, children } = props; const classes = useStyles(); const childElements = useElementFilter(children, c => c.getElements()); @@ -65,4 +72,4 @@ export const AboutField = ({ label, value, gridSizes, children }: Props) => { {content} ); -}; +} diff --git a/plugins/catalog/src/components/AboutCard/index.ts b/plugins/catalog/src/components/AboutCard/index.ts index 84f6a02380..9660a5e2e2 100644 --- a/plugins/catalog/src/components/AboutCard/index.ts +++ b/plugins/catalog/src/components/AboutCard/index.ts @@ -15,5 +15,8 @@ */ export { AboutCard } from './AboutCard'; +export type { AboutCardProps } from './AboutCard'; export { AboutContent } from './AboutContent'; +export type { AboutContentProps } from './AboutContent'; export { AboutField } from './AboutField'; +export type { AboutFieldProps } from './AboutField'; diff --git a/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx b/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx index 94b9f5eee4..fe32e78959 100644 --- a/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx +++ b/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx @@ -16,13 +16,14 @@ import React from 'react'; import { Outlet } from 'react-router'; -import { - useEntityFromUrl, - AsyncEntityProvider, -} from '@backstage/plugin-catalog-react'; +import { AsyncEntityProvider } from '@backstage/plugin-catalog-react'; +import { useEntityFromUrl } from './useEntityFromUrl'; -export const CatalogEntityPage = () => ( - - - -); +/** @public */ +export function CatalogEntityPage() { + return ( + + + + ); +} diff --git a/plugins/catalog/src/components/CatalogEntityPage/index.ts b/plugins/catalog/src/components/CatalogEntityPage/index.ts index bac02a1529..f57648ae00 100644 --- a/plugins/catalog/src/components/CatalogEntityPage/index.ts +++ b/plugins/catalog/src/components/CatalogEntityPage/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { CatalogEntityPage } from './CatalogEntityPage'; diff --git a/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts b/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts new file mode 100644 index 0000000000..605305882e --- /dev/null +++ b/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + errorApiRef, + useApi, + useRouteRefParams, +} from '@backstage/core-plugin-api'; +import { + catalogApiRef, + EntityLoadingStatus, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import useAsyncRetry from 'react-use/lib/useAsyncRetry'; + +export const useEntityFromUrl = (): EntityLoadingStatus => { + const { kind, namespace, name } = useRouteRefParams(entityRouteRef); + const navigate = useNavigate(); + const errorApi = useApi(errorApiRef); + const catalogApi = useApi(catalogApiRef); + + const { + value: entity, + error, + loading, + retry: refresh, + } = useAsyncRetry( + () => catalogApi.getEntityByName({ kind, namespace, name }), + [catalogApi, kind, namespace, name], + ); + + useEffect(() => { + if (!name) { + errorApi.post(new Error('No name provided!')); + navigate('/'); + } + }, [errorApi, navigate, error, loading, entity, name]); + + return { entity, loading, error, refresh }; +}; diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index b320d6d220..45d42d0ea6 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; +import { GetEntityFacetsResponse } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, @@ -23,8 +24,13 @@ import { MockEntityListContextProvider, } from '@backstage/plugin-catalog-react'; import { ApiProvider } from '@backstage/core-app-api'; -import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; +import { + MockErrorApi, + renderWithEffects, + TestApiRegistry, +} from '@backstage/test-utils'; import { CatalogKindHeader } from './CatalogKindHeader'; +import { errorApiRef } from '@backstage/core-plugin-api'; const entities: Entity[] = [ { @@ -56,13 +62,24 @@ const entities: Entity[] = [ }, }, ]; - -const apis = TestApiRegistry.from([ - catalogApiRef, - { - getEntities: jest.fn().mockResolvedValue({ items: entities }), - }, -]); +const errorApi = new MockErrorApi(); +const apis = TestApiRegistry.from( + [ + catalogApiRef, + { + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { + kind: [ + { value: 'Component', count: 2 }, + { value: 'Template', count: 1 }, + { value: 'System', count: 1 }, + ], + }, + } as GetEntityFacetsResponse), + }, + ], + [errorApiRef, errorApi], +); describe('', () => { it('renders available kinds', async () => { diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx index 41c5888d79..ed54f1fa84 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx @@ -25,10 +25,12 @@ import { Theme, } from '@material-ui/core'; import { + catalogApiRef, EntityKindFilter, - useEntityKinds, - useEntityListProvider, + useEntityList, } from '@backstage/plugin-catalog-react'; +import useAsync from 'react-use/lib/useAsync'; +import { useApi } from '@backstage/core-plugin-api'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -38,16 +40,26 @@ const useStyles = makeStyles((theme: Theme) => }), ); -type CatalogKindHeaderProps = { +/** + * Props for {@link CatalogKindHeader}. + * + * @public + */ +export interface CatalogKindHeaderProps { initialFilter?: string; -}; +} -export const CatalogKindHeader = ({ - initialFilter = 'component', -}: CatalogKindHeaderProps) => { +/** @public */ +export function CatalogKindHeader(props: CatalogKindHeaderProps) { + const { initialFilter = 'component' } = props; const classes = useStyles(); - const { kinds: allKinds = [] } = useEntityKinds(); - const { updateFilters, queryParameters } = useEntityListProvider(); + const catalogApi = useApi(catalogApiRef); + const { value: allKinds } = useAsync(async () => { + return await catalogApi + .getEntityFacets({ facets: ['kind'] }) + .then(response => response.facets.kind?.map(f => f.value).sort() || []); + }); + const { updateFilters, queryParameters } = useEntityList(); const [selectedKind, setSelectedKind] = useState( ([queryParameters.kind].flat()[0] ?? initialFilter).toLocaleLowerCase( @@ -67,7 +79,7 @@ export const CatalogKindHeader = ({ // including selectedKind if it's unknown - but allows the selectedKind to get clobbered by the // more proper catalog kind if it exists. const options = [capitalize(selectedKind)] - .concat(allKinds) + .concat(allKinds ?? []) .sort() .reduce((acc, kind) => { acc[kind.toLocaleLowerCase('en-US')] = kind; @@ -88,4 +100,4 @@ export const CatalogKindHeader = ({ ))} ); -}; +} diff --git a/plugins/catalog/src/components/CatalogKindHeader/index.ts b/plugins/catalog/src/components/CatalogKindHeader/index.ts index 0078207d58..5a5b20f1a0 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/index.ts +++ b/plugins/catalog/src/components/CatalogKindHeader/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { CatalogKindHeader } from './CatalogKindHeader'; +export type { CatalogKindHeaderProps } from './CatalogKindHeader'; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index cf49cff244..155bcc05df 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -21,8 +21,8 @@ import { DefaultCatalogPageProps, } from './DefaultCatalogPage'; -export const CatalogPage = (props: DefaultCatalogPageProps) => { +export function CatalogPage(props: DefaultCatalogPageProps) { const outlet = useOutlet(); return outlet || ; -}; +} diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index 20acff3c50..cf2eedb14c 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -29,9 +29,9 @@ import { } from '@backstage/core-plugin-api'; import { catalogApiRef, - DefaultStarredEntitiesApi, entityRouteRef, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { mockBreakpoint, @@ -44,7 +44,7 @@ import DashboardIcon from '@material-ui/icons/Dashboard'; import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { createComponentRouteRef } from '../../routes'; -import { EntityRow } from '../CatalogTable'; +import { CatalogTableRow } from '../CatalogTable'; import { DefaultCatalogPage } from './DefaultCatalogPage'; describe('DefaultCatalogPage', () => { @@ -73,7 +73,8 @@ describe('DefaultCatalogPage', () => { relations: [ { type: RELATION_OWNED_BY, - target: { kind: 'Group', name: 'tools', namespace: 'default' }, + targetRef: 'group:default/tools', + target: { kind: 'group', name: 'tools', namespace: 'default' }, }, ], }, @@ -90,8 +91,9 @@ describe('DefaultCatalogPage', () => { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'group:default/not-tools', target: { - kind: 'Group', + kind: 'group', name: 'not-tools', namespace: 'default', }, @@ -100,8 +102,8 @@ describe('DefaultCatalogPage', () => { }, ] as Entity[], }), - getLocationByEntity: () => - Promise.resolve({ id: 'id', type: 'github', target: 'url' }), + getLocationByRef: () => + Promise.resolve({ id: 'id', type: 'url', target: 'url' }), getEntityByName: async entityName => { return { apiVersion: 'backstage.io/v1alpha1', @@ -110,6 +112,7 @@ describe('DefaultCatalogPage', () => { relations: [ { type: RELATION_MEMBER_OF, + targetRef: 'group:default/tools', target: { namespace: 'default', kind: 'Group', name: 'tools' }, }, ], @@ -138,10 +141,7 @@ describe('DefaultCatalogPage', () => { [catalogApiRef, catalogApi], [identityApiRef, identityApi], [storageApiRef, storageApi], - [ - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi }), - ], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], ]} > {children} @@ -180,7 +180,7 @@ describe('DefaultCatalogPage', () => { }, 20_000); it('should render the custom column passed as prop', async () => { - const columns: TableColumn[] = [ + const columns: TableColumn[] = [ { title: 'Foo', field: 'entity.foo' }, { title: 'Bar', field: 'entity.bar' }, { title: 'Baz', field: 'entity.spec.lifecycle' }, @@ -208,7 +208,7 @@ describe('DefaultCatalogPage', () => { }, 20_000); it('should render the custom actions of an item passed as prop', async () => { - const actions: TableProps['actions'] = [ + const actions: TableProps['actions'] = [ () => { return { icon: () => , diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index 1453a39bbc..8bb9bae764 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -35,8 +35,7 @@ import { } from '@backstage/plugin-catalog-react'; import React from 'react'; import { createComponentRouteRef } from '../../routes'; -import { CatalogTable } from '../CatalogTable'; -import { EntityRow } from '../CatalogTable/types'; +import { CatalogTable, CatalogTableRow } from '../CatalogTable'; import { FilteredEntityLayout, EntityListContainer, @@ -45,20 +44,18 @@ import { import { CatalogKindHeader } from '../CatalogKindHeader'; /** - * DefaultCatalogPageProps + * Props for root catalog pages. + * * @public */ -export type DefaultCatalogPageProps = { +export interface DefaultCatalogPageProps { initiallySelectedFilter?: UserListFilterKind; - columns?: TableColumn[]; - actions?: TableProps['actions']; -}; + columns?: TableColumn[]; + actions?: TableProps['actions']; +} -export const DefaultCatalogPage = ({ - columns, - actions, - initiallySelectedFilter = 'owned', -}: DefaultCatalogPageProps) => { +export function DefaultCatalogPage(props: DefaultCatalogPageProps) { + const { columns, actions, initiallySelectedFilter = 'owned' } = props; const orgName = useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage'; const createComponentLink = useRouteRef(createComponentRouteRef); @@ -90,4 +87,4 @@ export const DefaultCatalogPage = ({ ); -}; +} diff --git a/plugins/catalog/src/components/CatalogPage/index.ts b/plugins/catalog/src/components/CatalogPage/index.ts index ca6a13c034..9a3d083f41 100644 --- a/plugins/catalog/src/components/CatalogPage/index.ts +++ b/plugins/catalog/src/components/CatalogPage/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { CatalogPage } from './CatalogPage'; export { DefaultCatalogPage } from './DefaultCatalogPage'; export type { DefaultCatalogPageProps } from './DefaultCatalogPage'; diff --git a/plugins/catalog/src/components/CatalogResultListItem/CatalogResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx similarity index 70% rename from plugins/catalog/src/components/CatalogResultListItem/CatalogResultListItem.tsx rename to plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index f0a32ec0e2..54cfe43364 100644 --- a/plugins/catalog/src/components/CatalogResultListItem/CatalogResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -24,6 +24,7 @@ import { makeStyles, } from '@material-ui/core'; import { Link } from '@backstage/core-components'; +import { IndexableDocument } from '@backstage/search-common'; const useStyles = makeStyles({ flexContainer: { @@ -36,7 +37,21 @@ const useStyles = makeStyles({ }, }); -export const CatalogResultListItem = ({ result }: any) => { +/** + * Props for {@link CatalogSearchResultListItem}. + * + * @public + */ +export interface CatalogSearchResultListItemProps { + result: IndexableDocument; +} + +/** @public */ +export function CatalogSearchResultListItem( + props: CatalogSearchResultListItemProps, +) { + const result = props.result as any; + const classes = useStyles(); return ( @@ -57,4 +72,16 @@ export const CatalogResultListItem = ({ result }: any) => { ); -}; +} + +/** + * @public + * @deprecated use {@link CatalogSearchResultListItemProps} instead + */ +export type CatalogResultListItemProps = CatalogSearchResultListItemProps; + +/** + * @public + * @deprecated use {@link CatalogSearchResultListItem} instead + */ +export const CatalogResultListItem = CatalogSearchResultListItem; diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/index.ts b/plugins/catalog/src/components/CatalogSearchResultListItem/index.ts new file mode 100644 index 0000000000..0396117825 --- /dev/null +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + CatalogSearchResultListItem, + CatalogResultListItem, +} from './CatalogSearchResultListItem'; +export type { + CatalogSearchResultListItemProps, + CatalogResultListItemProps, +} from './CatalogSearchResultListItem'; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 4c6adcf400..cc05bbe2ae 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -15,23 +15,19 @@ */ 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 { entityRouteRef, - DefaultStarredEntitiesApi, MockEntityListContextProvider, starredEntitiesApiRef, UserListFilter, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; -import { - MockStorageApi, - renderInTestApp, - TestApiRegistry, -} from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import * as React from 'react'; import { CatalogTable } from './CatalogTable'; @@ -57,7 +53,7 @@ const entities: Entity[] = [ describe('CatalogTable component', () => { const mockApis = TestApiRegistry.from([ starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), + new MockStarredEntitiesApi(), ]); beforeEach(() => { @@ -123,7 +119,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 +151,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/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index f293710847..4ef51d9945 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -13,23 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RELATION_OWNED_BY, RELATION_PART_OF } from '@backstage/catalog-model'; +import { + ANNOTATION_EDIT_URL, + ANNOTATION_VIEW_URL, + RELATION_OWNED_BY, + RELATION_PART_OF, +} from '@backstage/catalog-model'; import { favoriteEntityIcon, favoriteEntityTooltip, - formatEntityRefTitle, - getEntityMetadataEditUrl, - getEntityMetadataViewUrl, + humanizeEntityRef, getEntityRelations, - useEntityListProvider, + useEntityList, useStarredEntities, } from '@backstage/plugin-catalog-react'; import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; import { capitalize } from 'lodash'; import React, { useMemo } from 'react'; -import * as columnFactories from './columns'; -import { EntityRow } from './types'; +import { columnFactories } from './columns'; +import { CatalogTableRow } from './types'; import { CodeSnippet, Table, @@ -38,16 +41,23 @@ import { WarningPanel, } from '@backstage/core-components'; -type CatalogTableProps = { - columns?: TableColumn[]; - actions?: TableProps['actions']; -}; +/** + * Props for {@link CatalogTable}. + * + * @public + */ +export interface CatalogTableProps { + columns?: TableColumn[]; + actions?: TableProps['actions']; +} -export const CatalogTable = ({ columns, actions }: CatalogTableProps) => { +/** @public */ +export const CatalogTable = (props: CatalogTableProps) => { + const { columns, actions } = props; const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); - const { loading, error, entities, filters } = useEntityListProvider(); + const { loading, error, entities, filters } = useEntityList(); - const defaultColumns: TableColumn[] = useMemo( + const defaultColumns: TableColumn[] = useMemo( () => [ columnFactories.createNameColumn({ defaultKind: filters.kind?.value }), columnFactories.createSystemColumn(), @@ -77,9 +87,9 @@ export const CatalogTable = ({ columns, actions }: CatalogTableProps) => { ); } - const defaultActions: TableProps['actions'] = [ + const defaultActions: TableProps['actions'] = [ ({ entity }) => { - const url = getEntityMetadataViewUrl(entity); + const url = entity.metadata.annotations?.[ANNOTATION_VIEW_URL]; return { icon: () => , tooltip: 'View', @@ -91,7 +101,7 @@ export const CatalogTable = ({ columns, actions }: CatalogTableProps) => { }; }, ({ entity }) => { - const url = getEntityMetadataEditUrl(entity); + const url = entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; return { icon: () => , tooltip: 'Edit', @@ -122,16 +132,16 @@ export const CatalogTable = ({ columns, actions }: CatalogTableProps) => { return { entity, resolved: { - name: formatEntityRefTitle(entity, { + name: humanizeEntityRef(entity, { defaultKind: 'Component', }), ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .map(r => humanizeEntityRef(r, { defaultKind: 'group' })) .join(', '), ownedByRelations, partOfSystemRelationTitle: partOfSystemRelations .map(r => - formatEntityRefTitle(r, { + humanizeEntityRef(r, { defaultKind: 'system', }), ) @@ -148,7 +158,7 @@ export const CatalogTable = ({ columns, actions }: CatalogTableProps) => { const showPagination = rows.length > 20; return ( - + isLoading={loading} columns={columns || defaultColumns} options={{ diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 683ac33132..0fb91d4f3f 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -15,125 +15,120 @@ */ import React from 'react'; import { - formatEntityRefTitle, + humanizeEntityRef, EntityRefLink, EntityRefLinks, } from '@backstage/plugin-catalog-react'; import { Chip } from '@material-ui/core'; -import { EntityRow } from './types'; +import { CatalogTableRow } from './types'; import { OverflowTooltip, TableColumn } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; -type NameColumnProps = { - defaultKind?: string; -}; +// The columnFactories symbol is not directly exported, but through the +// CatalogTable.columns field. +/** @public */ +export const columnFactories = Object.freeze({ + createNameColumn(options?: { + defaultKind?: string; + }): TableColumn { + function formatContent(entity: Entity): string { + return ( + entity.metadata?.title || + humanizeEntityRef(entity, { + defaultKind: options?.defaultKind, + }) + ); + } -export function createNameColumn( - props?: NameColumnProps, -): TableColumn { - function formatContent(entity: Entity): string { - return ( - entity.metadata?.title || - formatEntityRefTitle(entity, { - defaultKind: props?.defaultKind, - }) - ); - } - - return { - title: 'Name', - field: 'resolved.name', - highlight: true, - customSort({ entity: entity1 }, { entity: entity2 }) { - // TODO: We could implement this more efficiently by comparing field by field. - // This has similar issues as above. - return formatContent(entity1).localeCompare(formatContent(entity2)); - }, - render: ({ entity }) => ( - - ), - }; -} - -export function createSystemColumn(): TableColumn { - return { - title: 'System', - field: 'resolved.partOfSystemRelationTitle', - render: ({ resolved }) => ( - - ), - }; -} - -export function createOwnerColumn(): TableColumn { - return { - title: 'Owner', - field: 'resolved.ownedByRelationsTitle', - render: ({ resolved }) => ( - - ), - }; -} - -export function createSpecTypeColumn(): TableColumn { - return { - title: 'Type', - field: 'entity.spec.type', - hidden: true, - }; -} - -export function createSpecLifecycleColumn(): TableColumn { - return { - title: 'Lifecycle', - field: 'entity.spec.lifecycle', - }; -} - -export function createMetadataDescriptionColumn(): TableColumn { - return { - title: 'Description', - field: 'entity.metadata.description', - render: ({ entity }) => ( - - ), - width: 'auto', - }; -} - -export function createTagsColumn(): TableColumn { - return { - title: 'Tags', - field: 'entity.metadata.tags', - cellStyle: { - padding: '0px 16px 0px 20px', - }, - render: ({ entity }) => ( - <> - {entity.metadata.tags && - entity.metadata.tags.map(t => ( - - ))} - - ), - }; -} + return { + title: 'Name', + field: 'resolved.name', + highlight: true, + customSort({ entity: entity1 }, { entity: entity2 }) { + // TODO: We could implement this more efficiently by comparing field by field. + // This has similar issues as above. + return formatContent(entity1).localeCompare(formatContent(entity2)); + }, + render: ({ entity }) => ( + + ), + }; + }, + createSystemColumn(): TableColumn { + return { + title: 'System', + field: 'resolved.partOfSystemRelationTitle', + render: ({ resolved }) => ( + + ), + }; + }, + createOwnerColumn(): TableColumn { + return { + title: 'Owner', + field: 'resolved.ownedByRelationsTitle', + render: ({ resolved }) => ( + + ), + }; + }, + createSpecTypeColumn(): TableColumn { + return { + title: 'Type', + field: 'entity.spec.type', + hidden: true, + }; + }, + createSpecLifecycleColumn(): TableColumn { + return { + title: 'Lifecycle', + field: 'entity.spec.lifecycle', + }; + }, + createMetadataDescriptionColumn(): TableColumn { + return { + title: 'Description', + field: 'entity.metadata.description', + render: ({ entity }) => ( + + ), + width: 'auto', + }; + }, + createTagsColumn(): TableColumn { + return { + title: 'Tags', + field: 'entity.metadata.tags', + cellStyle: { + padding: '0px 16px 0px 20px', + }, + render: ({ entity }) => ( + <> + {entity.metadata.tags && + entity.metadata.tags.map(t => ( + + ))} + + ), + }; + }, +}); diff --git a/plugins/catalog/src/components/CatalogTable/index.ts b/plugins/catalog/src/components/CatalogTable/index.ts index e1660d6c5a..4e1b90f80a 100644 --- a/plugins/catalog/src/components/CatalogTable/index.ts +++ b/plugins/catalog/src/components/CatalogTable/index.ts @@ -15,4 +15,5 @@ */ export { CatalogTable } from './CatalogTable'; -export type { EntityRow } from './types'; +export type { CatalogTableProps } from './CatalogTable'; +export type { CatalogTableRow } from './types'; diff --git a/plugins/catalog/src/components/CatalogTable/types.ts b/plugins/catalog/src/components/CatalogTable/types.ts index 85926becc3..7eaff6faed 100644 --- a/plugins/catalog/src/components/CatalogTable/types.ts +++ b/plugins/catalog/src/components/CatalogTable/types.ts @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, EntityName } from '@backstage/catalog-model'; -export type EntityRow = { +/** @public */ +export interface CatalogTableRow { entity: Entity; resolved: { name: string; @@ -24,4 +26,4 @@ export type EntityRow = { ownedByRelationsTitle?: string; ownedByRelations: EntityName[]; }; -}; +} diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx index e43104b67d..05d836870f 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx @@ -81,10 +81,11 @@ describe('', () => { relations: [ { target: { - kind: 'Component', + kind: 'component', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'component:my-namespace/target-name', type: RELATION_DEPENDENCY_OF, }, ], diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx index 34057d7bed..b85a446b75 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx @@ -23,15 +23,16 @@ import { RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -type Props = { +/** @public */ +export interface DependencyOfComponentsCardProps { variant?: 'gridItem'; title?: string; -}; +} -export const DependencyOfComponentsCard = ({ - variant = 'gridItem', - title = 'Dependency of components', -}: Props) => { +export function DependencyOfComponentsCard( + props: DependencyOfComponentsCardProps, +) { + const { variant = 'gridItem', title = 'Dependency of components' } = props; return ( ); -}; +} diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/index.ts b/plugins/catalog/src/components/DependencyOfComponentsCard/index.ts index bf953fe986..3d324e865d 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/index.ts +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/index.ts @@ -15,3 +15,4 @@ */ export { DependencyOfComponentsCard } from './DependencyOfComponentsCard'; +export type { DependencyOfComponentsCardProps } from './DependencyOfComponentsCard'; diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx index b654136d18..b7ef594522 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx @@ -81,10 +81,11 @@ describe('', () => { relations: [ { target: { - kind: 'Component', + kind: 'component', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'component:my-namespace/target-name', type: RELATION_DEPENDS_ON, }, ], diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx index 7e176139ad..33e8caf870 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx @@ -23,15 +23,14 @@ import { RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -type Props = { +/** @public */ +export interface DependsOnComponentsCardProps { variant?: 'gridItem'; title?: string; -}; +} -export const DependsOnComponentsCard = ({ - variant = 'gridItem', - title = 'Depends on components', -}: Props) => { +export function DependsOnComponentsCard(props: DependsOnComponentsCardProps) { + const { variant = 'gridItem', title = 'Depends on components' } = props; return ( ); -}; +} diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/index.ts b/plugins/catalog/src/components/DependsOnComponentsCard/index.ts index 96525fe241..8292526270 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/index.ts +++ b/plugins/catalog/src/components/DependsOnComponentsCard/index.ts @@ -15,3 +15,4 @@ */ export { DependsOnComponentsCard } from './DependsOnComponentsCard'; +export type { DependsOnComponentsCardProps } from './DependsOnComponentsCard'; diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx index 40685705aa..b8d2801e48 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx @@ -81,10 +81,11 @@ describe('', () => { relations: [ { target: { - kind: 'Resource', + kind: 'resource', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'resource:my-namespace/target-name', type: RELATION_DEPENDS_ON, }, ], diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx index 34a8e5824d..3fe29093fe 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx @@ -23,11 +23,13 @@ import { resourceEntityColumns, } from '../RelatedEntitiesCard'; -type Props = { +/** @public */ +export interface DependsOnResourcesCardProps { variant?: 'gridItem'; -}; +} -export const DependsOnResourcesCard = ({ variant = 'gridItem' }: Props) => { +export function DependsOnResourcesCard(props: DependsOnResourcesCardProps) { + const { variant = 'gridItem' } = props; return ( { asRenderableEntities={asResourceEntities} /> ); -}; +} diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/index.ts b/plugins/catalog/src/components/DependsOnResourcesCard/index.ts index 5ff94d3542..8d638e16a2 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/index.ts +++ b/plugins/catalog/src/components/DependsOnResourcesCard/index.ts @@ -15,3 +15,4 @@ */ export { DependsOnResourcesCard } from './DependsOnResourcesCard'; +export type { DependsOnResourcesCardProps } from './DependsOnResourcesCard'; diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index 7bda1f4629..ee677c5238 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -41,30 +41,31 @@ const useStyles = makeStyles({ // NOTE(freben): Intentionally not exported at this point, since it's part of // the unstable extra context menu items concept below -type ExtraContextMenuItem = { +interface ExtraContextMenuItem { title: string; Icon: IconComponent; onClick: () => void; -}; +} // unstable context menu option, eg: disable the unregister entity menu -type contextMenuOptions = { +interface contextMenuOptions { disableUnregister: boolean; -}; +} -type Props = { +interface EntityContextMenuProps { UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; UNSTABLE_contextMenuOptions?: contextMenuOptions; onUnregisterEntity: () => void; onInspectEntity: () => void; -}; +} -export const EntityContextMenu = ({ - UNSTABLE_extraContextMenuItems, - UNSTABLE_contextMenuOptions, - onUnregisterEntity, - onInspectEntity, -}: Props) => { +export function EntityContextMenu(props: EntityContextMenuProps) { + const { + UNSTABLE_extraContextMenuItems, + UNSTABLE_contextMenuOptions, + onUnregisterEntity, + onInspectEntity, + } = props; const [anchorEl, setAnchorEl] = useState(); const classes = useStyles(); const unregisterPermission = useEntityPermission( @@ -150,4 +151,4 @@ export const EntityContextMenu = ({ ); -}; +} diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 78dcf855e8..c1deaf4b74 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -21,15 +21,14 @@ import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; import { AsyncEntityProvider, catalogApiRef, - DefaultStarredEntitiesApi, EntityProvider, entityRouteRef, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { MockPermissionApi, - MockStorageApi, renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; @@ -48,10 +47,7 @@ const mockEntity = { const mockApis = TestApiRegistry.from( [catalogApiRef, {} as CatalogApi], [alertApiRef, {} as AlertApi], - [ - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], [permissionApiRef, new MockPermissionApi()], ); diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 144e5d01a8..40c4f9e873 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -16,7 +16,7 @@ import { Entity, - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, RELATION_OWNED_BY, } from '@backstage/catalog-model'; import { @@ -35,21 +35,22 @@ import { useElementFilter, } from '@backstage/core-plugin-api'; import { - EntityContext, EntityRefLinks, FavoriteEntity, getEntityRelations, InspectEntityDialog, UnregisterEntityDialog, + useAsyncEntity, useEntityCompoundName, } from '@backstage/plugin-catalog-react'; import { Box, TabProps } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; -type SubRoute = { +/** @public */ +export type EntityLayoutRouteProps = { path: string; title: string; children: JSX.Element; @@ -59,19 +60,15 @@ type SubRoute = { const dataKey = 'plugin.catalog.entityLayoutRoute'; -const Route: (props: SubRoute) => null = () => null; +const Route: (props: EntityLayoutRouteProps) => null = () => null; attachComponentData(Route, dataKey, true); +attachComponentData(Route, 'core.gatherMountPoints', true); // This causes all mount points that are discovered within this route to use the path of the route itself -// This causes all mount points that are discovered within this route to use the path of the route itself -attachComponentData(Route, 'core.gatherMountPoints', true); - -const EntityLayoutTitle = ({ - entity, - title, -}: { +function EntityLayoutTitle(props: { title: string; entity: Entity | undefined; -}) => { +}) { + const { entity, title } = props; return ( } ); -}; +} -const headerProps = ( +function headerProps( paramKind: string | undefined, paramNamespace: string | undefined, paramName: string | undefined, entity: Entity | undefined, -): { headerTitle: string; headerType: string } => { +): { headerTitle: string; headerType: string } { const kind = paramKind ?? entity?.kind ?? ''; const namespace = paramNamespace ?? entity?.metadata.namespace ?? ''; const name = entity?.metadata.title ?? paramName ?? entity?.metadata.name ?? ''; return { headerTitle: `${name}${ - namespace && namespace !== ENTITY_DEFAULT_NAMESPACE - ? ` in ${namespace}` - : '' + namespace && namespace !== DEFAULT_NAMESPACE ? ` in ${namespace}` : '' }`, headerType: (() => { let t = kind.toLocaleLowerCase('en-US'); @@ -112,9 +107,10 @@ const headerProps = ( return t; })(), }; -}; +} -const EntityLabels = ({ entity }: { entity: Entity }) => { +function EntityLabels(props: { entity: Entity }) { + const { entity } = props; const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); return ( <> @@ -135,26 +131,27 @@ const EntityLabels = ({ entity }: { entity: Entity }) => { )} ); -}; +} // NOTE(freben): Intentionally not exported at this point, since it's part of // the unstable extra context menu items concept below -type ExtraContextMenuItem = { +interface ExtraContextMenuItem { title: string; Icon: IconComponent; onClick: () => void; -}; +} // unstable context menu option, eg: disable the unregister entity menu -type contextMenuOptions = { +interface contextMenuOptions { disableUnregister: boolean; -}; +} -type EntityLayoutProps = { +/** @public */ +export interface EntityLayoutProps { UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; UNSTABLE_contextMenuOptions?: contextMenuOptions; children?: React.ReactNode; -}; +} /** * EntityLayout is a compound component, which allows you to define a layout for @@ -170,14 +167,17 @@ type EntityLayoutProps = { * * * ``` + * + * @public */ -export const EntityLayout = ({ - UNSTABLE_extraContextMenuItems, - UNSTABLE_contextMenuOptions, - children, -}: EntityLayoutProps) => { +export const EntityLayout = (props: EntityLayoutProps) => { + const { + UNSTABLE_extraContextMenuItems, + UNSTABLE_contextMenuOptions, + children, + } = props; const { kind, namespace, name } = useEntityCompoundName(); - const { entity, loading, error } = useContext(EntityContext); + const { entity, loading, error } = useAsyncEntity(); const location = useLocation(); const routes = useElementFilter( children, @@ -188,18 +188,20 @@ export const EntityLayout = ({ withStrictError: 'Child of EntityLayout must be an EntityLayout.Route', }) - .getElements() // all nodes, element data, maintain structure or not? - .flatMap(({ props }) => { - if (props.if && entity && !props.if(entity)) { + .getElements() // all nodes, element data, maintain structure or not? + .flatMap(({ props: elementProps }) => { + if (!entity) { + return []; + } else if (elementProps.if && !elementProps.if(entity)) { return []; } return [ { - path: props.path, - title: props.title, - children: props.children, - tabProps: props.tabProps, + path: elementProps.path, + title: elementProps.title, + children: elementProps.children, + tabProps: elementProps.tabProps, }, ]; }), diff --git a/plugins/catalog/src/components/EntityLayout/index.ts b/plugins/catalog/src/components/EntityLayout/index.ts index 3b0962ae36..251d213637 100644 --- a/plugins/catalog/src/components/EntityLayout/index.ts +++ b/plugins/catalog/src/components/EntityLayout/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { EntityLayout } from './EntityLayout'; +export type { EntityLayoutProps, EntityLayoutRouteProps } from './EntityLayout'; diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index 20229ceb14..4c4a1ac908 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -14,25 +14,23 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; import LanguageIcon from '@material-ui/icons/Language'; import React from 'react'; import { EntityLinksEmptyState } from './EntityLinksEmptyState'; import { LinksGridList } from './LinksGridList'; import { ColumnBreakpoints } from './types'; - import { IconComponent, useApp } from '@backstage/core-plugin-api'; import { InfoCard } from '@backstage/core-components'; -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; +/** @public */ +export interface EntityLinksCardProps { cols?: ColumnBreakpoints | number; variant?: 'gridItem'; -}; +} -export const EntityLinksCard = ({ cols = undefined, variant }: Props) => { +export function EntityLinksCard(props: EntityLinksCardProps) { + const { cols = undefined, variant } = props; const { entity } = useEntity(); const app = useApp(); @@ -57,4 +55,4 @@ export const EntityLinksCard = ({ cols = undefined, variant }: Props) => { )} ); -}; +} diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx index 54325ffb5d..7f95a66700 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx @@ -40,7 +40,7 @@ const useStyles = makeStyles( { name: 'PluginCatalogEntityLinksEmptyState' }, ); -export const EntityLinksEmptyState = () => { +export function EntityLinksEmptyState() { const classes = useStyles(); return ( @@ -68,4 +68,4 @@ export const EntityLinksEmptyState = () => { ); -}; +} diff --git a/plugins/catalog/src/components/EntityLinksCard/IconLink.tsx b/plugins/catalog/src/components/EntityLinksCard/IconLink.tsx index 9a0302a64e..9afdd1f1da 100644 --- a/plugins/catalog/src/components/EntityLinksCard/IconLink.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/IconLink.tsx @@ -17,7 +17,6 @@ import { makeStyles, Box, Typography } from '@material-ui/core'; import LanguageIcon from '@material-ui/icons/Language'; import React from 'react'; - import { Link } from '@backstage/core-components'; import { IconComponent } from '@backstage/core-plugin-api'; @@ -32,15 +31,12 @@ const useStyles = makeStyles({ }, }); -export const IconLink = ({ - href, - text, - Icon, -}: { +export function IconLink(props: { href: string; text?: string; Icon?: IconComponent; -}) => { +}) { + const { href, text, Icon } = props; const classes = useStyles(); return ( @@ -57,4 +53,4 @@ export const IconLink = ({ ); -}; +} diff --git a/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx index 3df38c4523..7861b177ce 100644 --- a/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx @@ -21,18 +21,19 @@ import { ColumnBreakpoints } from './types'; import { useDynamicColumns } from './useDynamicColumns'; import { IconComponent } from '@backstage/core-plugin-api'; -export type LinksGridListItem = { +export interface LinksGridListItem { href: string; text?: string; Icon?: IconComponent; -}; +} -type Props = { +interface LinksGridListProps { items: LinksGridListItem[]; cols?: ColumnBreakpoints | number; -}; +} -export const LinksGridList = ({ items, cols = undefined }: Props) => { +export function LinksGridList(props: LinksGridListProps) { + const { items, cols = undefined } = props; const numOfCols = useDynamicColumns(cols); return ( @@ -44,4 +45,4 @@ export const LinksGridList = ({ items, cols = undefined }: Props) => { ))} ); -}; +} diff --git a/plugins/catalog/src/components/EntityLinksCard/index.ts b/plugins/catalog/src/components/EntityLinksCard/index.ts index fc53904a47..dcd4366066 100644 --- a/plugins/catalog/src/components/EntityLinksCard/index.ts +++ b/plugins/catalog/src/components/EntityLinksCard/index.ts @@ -15,4 +15,5 @@ */ export { EntityLinksCard } from './EntityLinksCard'; +export type { EntityLinksCardProps } from './EntityLinksCard'; export type { EntityLinksEmptyStateClassKey } from './EntityLinksEmptyState'; diff --git a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.test.tsx b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.test.tsx index faaba13d78..4f16078eb9 100644 --- a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.test.tsx +++ b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.test.tsx @@ -16,7 +16,6 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; - import { EntityNotFound } from './EntityNotFound'; describe('', () => { diff --git a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx index e396e5b4aa..fca763c37b 100644 --- a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx +++ b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { Grid, Button, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import { BackstageTheme } from '@backstage/theme'; - import { Illo } from './Illo'; const useStyles = makeStyles(theme => ({ @@ -43,7 +42,7 @@ const useStyles = makeStyles(theme => ({ }, })); -export const EntityNotFound = () => { +export function EntityNotFound() { const classes = useStyles(); return ( @@ -67,4 +66,4 @@ export const EntityNotFound = () => { ); -}; +} diff --git a/plugins/catalog/src/components/EntityNotFound/Illo/Illo.tsx b/plugins/catalog/src/components/EntityNotFound/Illo/Illo.tsx index e0e76cd5d4..a375eca02a 100644 --- a/plugins/catalog/src/components/EntityNotFound/Illo/Illo.tsx +++ b/plugins/catalog/src/components/EntityNotFound/Illo/Illo.tsx @@ -34,7 +34,7 @@ const useStyles = makeStyles(theme => ({ }, })); -export const Illo = () => { +export function Illo() { const classes = useStyles(); return ( { alt="Illustration on entity not found page" /> ); -}; +} diff --git a/plugins/catalog/src/components/EntityNotFound/index.ts b/plugins/catalog/src/components/EntityNotFound/index.ts index 700a1097ca..aa2bd8e82b 100644 --- a/plugins/catalog/src/components/EntityNotFound/index.ts +++ b/plugins/catalog/src/components/EntityNotFound/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { EntityNotFound } from './EntityNotFound'; diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx index f58e6b9b66..c5442f3a3d 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + 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'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; - import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; describe('DeleteEntityDialog', () => { @@ -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/EntityOrphanWarning/DeleteEntityDialog.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx index 20aa673ea3..fc1391152a 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx @@ -21,19 +21,15 @@ import React, { useState } from 'react'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; -type Props = { +interface DeleteEntityDialogProps { open: boolean; onClose: () => any; onConfirm: () => any; entity: Entity; -}; +} -export const DeleteEntityDialog = ({ - open, - onClose, - onConfirm, - entity, -}: Props) => { +export function DeleteEntityDialog(props: DeleteEntityDialogProps) { + const { open, onClose, onConfirm, entity } = props; const [busy, setBusy] = useState(false); const catalogApi = useApi(catalogApiRef); const alertApi = useApi(alertApiRef); @@ -72,4 +68,4 @@ export const DeleteEntityDialog = ({ ); -}; +} diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx index a8aac405fe..f00474868f 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx @@ -14,14 +14,11 @@ * limitations under the License. */ -import { - catalogApiRef, - catalogRouteRef, - EntityProvider, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; +import { rootRouteRef } from '../../routes'; import { EntityOrphanWarning } from './EntityOrphanWarning'; describe('', () => { @@ -59,7 +56,7 @@ describe('', () => { , { mountedRoutes: { - '/create': catalogRouteRef, + '/create': rootRouteRef, }, }, ); diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx index c9d2d94609..26b0e5de45 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx @@ -15,22 +15,33 @@ */ import { Entity } from '@backstage/catalog-model'; -import { catalogRouteRef, useEntity } from '@backstage/plugin-catalog-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Alert } from '@material-ui/lab'; import React, { useState } from 'react'; import { useNavigate } from 'react-router'; import { DeleteEntityDialog } from './DeleteEntityDialog'; import { useRouteRef } from '@backstage/core-plugin-api'; - -export const isOrphan = (entity: Entity) => - entity?.metadata?.annotations?.['backstage.io/orphan'] === 'true'; +import { rootRouteRef } from '../../routes'; /** - * Displays a warning alert if the entity is marked as orphan with the ability to delete said entity. + * Returns true if the given entity has the orphan annotation given by the + * catalog. + * + * @public */ -export const EntityOrphanWarning = () => { +export function isOrphan(entity: Entity): boolean { + return entity?.metadata?.annotations?.['backstage.io/orphan'] === 'true'; +} + +/** + * Displays a warning alert if the entity is marked as orphan with the ability + * to delete said entity. + * + * @public + */ +export function EntityOrphanWarning() { const navigate = useNavigate(); - const catalogLink = useRouteRef(catalogRouteRef); + const catalogLink = useRouteRef(rootRouteRef); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const { entity } = useEntity(); @@ -53,4 +64,4 @@ export const EntityOrphanWarning = () => { /> ); -}; +} diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx deleted file mode 100644 index 82a980afcb..0000000000 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ /dev/null @@ -1,216 +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 { - Entity, - ENTITY_DEFAULT_NAMESPACE, - RELATION_OWNED_BY, -} from '@backstage/catalog-model'; -import { - EntityContext, - EntityRefLinks, - FavoriteEntity, - getEntityRelations, - InspectEntityDialog, - UnregisterEntityDialog, - useEntityCompoundName, -} from '@backstage/plugin-catalog-react'; -import { Box } from '@material-ui/core'; -import React, { useContext, useState } from 'react'; -import { useNavigate } from 'react-router'; -import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; -import { Tabbed } from './Tabbed'; - -import { - Content, - Header, - HeaderLabel, - Link, - Page, - Progress, - ResponseErrorPanel, - WarningPanel, -} from '@backstage/core-components'; - -import { IconComponent } from '@backstage/core-plugin-api'; - -const EntityPageTitle = ({ - entity, - title, -}: { - title: string; - entity: Entity | undefined; -}) => ( - - {title} - {entity && } - -); - -const EntityLabels = ({ entity }: { entity: Entity }) => { - const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); - return ( - <> - {ownedByRelations.length > 0 && ( - - } - /> - )} - {entity.spec?.lifecycle && ( - - )} - - ); -}; - -const headerProps = ( - kind: string, - namespace: string | undefined, - name: string, - entity: Entity | undefined, -): { headerTitle: string; headerType: string } => { - return { - headerTitle: `${name}${ - namespace && namespace !== ENTITY_DEFAULT_NAMESPACE - ? ` in ${namespace}` - : '' - }`, - headerType: (() => { - let t = kind.toLocaleLowerCase('en-US'); - if (entity && entity.spec && 'type' in entity.spec) { - t += ' — '; - t += (entity.spec as { type: string }).type.toLocaleLowerCase('en-US'); - } - return t; - })(), - }; -}; - -// NOTE(freben): Intentionally not exported at this point, since it's part of -// the unstable extra context menu items concept below -type ExtraContextMenuItem = { - title: string; - Icon: IconComponent; - onClick: () => void; -}; - -// unstable context menu option, eg: disable the unregister entity menu -type contextMenuOptions = { - disableUnregister: boolean; -}; - -type EntityPageLayoutProps = { - UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; - UNSTABLE_contextMenuOptions?: contextMenuOptions; - 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, - UNSTABLE_contextMenuOptions, -}: EntityPageLayoutProps) => { - const { kind, namespace, name } = useEntityCompoundName(); - const { entity, loading, error } = useContext(EntityContext); - const { headerTitle, headerType } = headerProps( - kind, - namespace, - name, - entity!, - ); - - const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); - const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false); - const navigate = useNavigate(); - const cleanUpAfterRemoval = async () => { - setConfirmationDialogOpen(false); - navigate('/'); - }; - - return ( - -
} - pageTitleOverride={headerTitle} - type={headerType} - > - {/* TODO: Make entity labels configurable for entity kind / type */} - {entity && ( - <> - - setConfirmationDialogOpen(true)} - onInspectEntity={() => setInspectionDialogOpen(true)} - /> - - )} -
- - {loading && ( - - - - )} - - {entity && {children}} - - {error && ( - - - - )} - - {!loading && !error && !entity && ( - - - There is no {kind} with the requested{' '} - - kind, namespace, and name - - . - - - )} - - setConfirmationDialogOpen(false)} - /> - setInspectionDialogOpen(false)} - /> -
- ); -}; - -EntityPageLayout.Content = Tabbed.Content; diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx deleted file mode 100644 index ad111380f8..0000000000 --- a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx +++ /dev/null @@ -1,197 +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 React from 'react'; -import { Tabbed } from './Tabbed'; -import { renderInTestApp } from '@backstage/test-utils'; -import { act, fireEvent } from '@testing-library/react'; -import { Routes, Route } from 'react-router'; - -describe('Tabbed layout', () => { - it('renders simplest case', async () => { - const rendered = await renderInTestApp( - - tabbed-test-content
} - /> - , - ); - - expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); - expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); - }); - - it('throws if any other component is a child of Tabbed.Layout', async () => { - await expect( - renderInTestApp( - - tabbed-test-content
} - /> -
This will cause app to throw
- , - ), - ).rejects.toThrow(/This component only accepts/); - }); - - it('navigates when user clicks different tab', async () => { - const rendered = await renderInTestApp( - - - tabbed-test-content
} - /> - tabbed-test-content-2} - /> - - } - /> - , - ); - - const secondTab = rendered.queryAllByRole('tab')[1]; - act(() => { - fireEvent.click(secondTab); - }); - - expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); - - expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); - }); - - describe('correctly delegates nested links', () => { - const renderRoute = (route: string) => - renderInTestApp( - - - tabbed-test-content} - /> - - tabbed-test-content-2 - - tabbed-test-nested-content-2} - /> - - - } - /> - - } - /> - , - { routeEntries: [route] }, - ); - - it('works for nested content', async () => { - const rendered = await renderRoute('/some-other-path/nested'); - - expect( - rendered.queryByText('tabbed-test-content'), - ).not.toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); - expect( - rendered.queryByText('tabbed-test-nested-content-2'), - ).toBeInTheDocument(); - }); - - it('works for non-nested content', async () => { - const rendered = await renderRoute('/some-other-path/'); - - expect( - rendered.queryByText('tabbed-test-content'), - ).not.toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); - expect( - rendered.queryByText('tabbed-test-nested-content-2'), - ).not.toBeInTheDocument(); - }); - }); - - it('shows only one tab contents at a time', async () => { - const rendered = await renderInTestApp( - - tabbed-test-content} - /> - tabbed-test-content-2} - /> - , - { routeEntries: ['/some-other-path'] }, - ); - - expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); - - expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); - }); - - it('redirects to the top level when no route is matching the url', async () => { - const rendered = await renderInTestApp( - - tabbed-test-content} - /> - tabbed-test-content-2} - /> - , - { routeEntries: ['/non-existing-path'] }, - ); - - expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); - expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); - expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); - - expect( - rendered.queryByText('tabbed-test-content-2'), - ).not.toBeInTheDocument(); - }); -}); diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx deleted file mode 100644 index 59271d2c60..0000000000 --- a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx +++ /dev/null @@ -1,127 +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 React from 'react'; -import { - useParams, - useNavigate, - PartialRouteObject, - matchRoutes, - RouteObject, - useRoutes, - Navigate, - RouteMatch, -} from 'react-router'; -import { Helmet } from 'react-helmet'; -import { Tab, HeaderTabs, Content } from '@backstage/core-components'; - -const getSelectedIndexOrDefault = ( - matchedRoute: RouteMatch, - tabs: Tab[], - defaultIndex = 0, -) => { - if (!matchedRoute) return defaultIndex; - const tabIndex = tabs.findIndex(t => t.id === matchedRoute.route.path); - return ~tabIndex ? tabIndex : defaultIndex; -}; - -/** - * Compound component, which allows you to define layout - * for EntityPage using Tabs as a sub-navigation mechanism - * Consists of 2 parts: Tabbed.Layout and Tabbed.Content. - * Takes care of: tabs, routes, document titles, spacing around content - * - * @example - * ```jsx - * - * This is rendered under /example/anything-here route} - * /> - * - * ``` - */ -export const Tabbed = { - Layout: ({ children }: { children: React.ReactNode }) => { - const routes: PartialRouteObject[] = []; - const tabs: Tab[] = []; - const params = useParams(); - const navigate = useNavigate(); - - React.Children.forEach(children, child => { - if (!React.isValidElement(child)) { - // Skip conditionals resolved to falses/nulls/undefineds etc - return; - } - if (child.type !== Tabbed.Content) { - throw new Error( - 'This component only accepts Content elements as direct children. Check the code of the EntityPage.', - ); - } - const pathAndId = (child as JSX.Element).props.path; - - // Child here must be then always a functional component without any wrappers - tabs.push({ - id: pathAndId, - label: (child as JSX.Element).props.title, - }); - - routes.push({ - path: pathAndId, - element: child.props.element, - }); - }); - - // Add catch-all for incorrect sub-routes - if ((routes?.[0]?.path ?? '') !== '') - routes.push({ - path: '/*', - element: , - }); - - const [matchedRoute] = - matchRoutes(routes as RouteObject[], `/${params['*']}`) ?? []; - const selectedIndex = getSelectedIndexOrDefault(matchedRoute, tabs); - const currentTab = tabs[selectedIndex]; - const title = currentTab?.label; - - const onTabChange = (index: number) => - // Remove trailing /* - // And remove leading / for relative navigation - // Note! route resolves relative to the position in the React tree, - // not relative to current location - navigate(tabs[index].id.replace(/\/\*$/, '').replace(/^\//, '')); - - const currentRouteElement = useRoutes(routes); - - if (!currentTab) return null; - return ( - <> - - - - {currentRouteElement} - - - ); - }, - Content: (_props: { path: string; title: string; element: JSX.Element }) => - null, -}; diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index e4df011b38..2eaf505ec0 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -19,7 +19,6 @@ import { AlphaEntity, stringifyEntityRef, EntityStatusItem, - compareEntityToRef, } from '@backstage/catalog-model'; import { catalogApiRef, @@ -42,12 +41,12 @@ const errorFilter = (i: EntityStatusItem) => i.level === 'error' && i.type === ENTITY_STATUS_CATALOG_PROCESSING_TYPE; -type GetOwnAndAncestorsErrorsResponse = { +interface GetOwnAndAncestorsErrorsResponse { items: { errors: SerializedError[]; entity: Entity; }[]; -}; +} async function getOwnAndAncestorsErrors( entityRef: string, @@ -67,10 +66,15 @@ async function getOwnAndAncestorsErrors( return { items }; } -export const hasCatalogProcessingErrors = async ( +/** + * Returns true if the given entity has any processing errors on it. + * + * @public + */ +export async function hasCatalogProcessingErrors( entity: Entity, context: { apis: ApiHolder }, -) => { +) { const catalogApi = context.apis.get(catalogApiRef); if (!catalogApi) { throw new Error(`No implementation available for ${catalogApiRef}`); @@ -81,12 +85,14 @@ export const hasCatalogProcessingErrors = async ( catalogApi, ); return errors.items.length > 0; -}; +} /** * Displays a list of errors from the ancestors of the current entity. + * + * @public */ -export const EntityProcessingErrorsPanel = () => { +export function EntityProcessingErrorsPanel() { const { entity } = useEntity(); const entityRef = stringifyEntityRef(entity); const catalogApi = useApi(catalogApiRef); @@ -110,10 +116,8 @@ export const EntityProcessingErrorsPanel = () => { <> {value.items.map((ancestorError, index) => ( - {!compareEntityToRef( - entity, - stringifyEntityRef(ancestorError.entity), - ) && ( + {stringifyEntityRef(entity) !== + stringifyEntityRef(ancestorError.entity) && ( The error below originates from{' '} @@ -126,4 +130,4 @@ export const EntityProcessingErrorsPanel = () => { ))} ); -}; +} diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index 87ee81c5f4..1b8667ce2a 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -15,8 +15,8 @@ */ import { Entity } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import React, { PropsWithChildren, ReactNode } from 'react'; +import { useAsyncEntity } from '@backstage/plugin-catalog-react'; +import React, { ReactNode, ReactElement } from 'react'; import { attachComponentData, useApiHolder, @@ -27,34 +27,46 @@ import useAsync from 'react-use/lib/useAsync'; const ENTITY_SWITCH_KEY = 'core.backstage.entitySwitch'; -const EntitySwitchCase = (_: { +/** @public */ +export interface EntitySwitchCaseProps { if?: ( entity: Entity, context: { apis: ApiHolder }, ) => boolean | Promise; children: ReactNode; -}) => null; +} -attachComponentData(EntitySwitchCase, ENTITY_SWITCH_KEY, true); +const EntitySwitchCaseComponent = (_props: EntitySwitchCaseProps) => null; -type SwitchCase = { +attachComponentData(EntitySwitchCaseComponent, ENTITY_SWITCH_KEY, true); + +interface EntitySwitchCase { if?: ( entity: Entity, context: { apis: ApiHolder }, ) => boolean | Promise; children: JSX.Element; -}; +} type SwitchCaseResult = { if: boolean | Promise; children: JSX.Element; }; -export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => { - const { entity } = useEntity(); +/** + * Props for the {@link EntitySwitch} component. + * @public + */ +export interface EntitySwitchProps { + children: ReactNode; +} + +/** @public */ +export const EntitySwitch = (props: EntitySwitchProps) => { + const { entity } = useAsyncEntity(); const apis = useApiHolder(); const results = useElementFilter( - children, + props.children, collection => collection .selectByComponentData({ @@ -62,9 +74,12 @@ export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => { withStrictError: 'Child of EntitySwitch is not an EntitySwitch.Case', }) .getElements() - .flatMap((element: React.ReactElement) => { + .flatMap((element: ReactElement) => { + if (!entity) { + return []; + } const { if: condition, children: elementsChildren } = - element.props as SwitchCase; + element.props as EntitySwitchCase; return [ { if: condition?.(entity, { apis }) ?? true, @@ -110,4 +125,4 @@ function AsyncEntitySwitch({ results }: { results: SwitchCaseResult[] }) { return value; } -EntitySwitch.Case = EntitySwitchCase; +EntitySwitch.Case = EntitySwitchCaseComponent; diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts index dd95a256bc..907aa45afe 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -22,13 +22,21 @@ function strCmp(a: string | undefined, b: string | undefined): boolean { ); } +/** + * For use in EntitySwitch.Case. Matches if the entity is of a given kind. + * @public + */ export function isKind(kind: string) { - return (entity: Entity) => strCmp(entity?.kind, kind); + return (entity: Entity) => strCmp(entity.kind, kind); } +/** + * For use in EntitySwitch.Case. Matches if the entity is a Component of a given spec.type. + * @public + */ export function isComponentType(type: string) { return (entity: Entity) => { - if (!strCmp(entity?.kind, 'component')) { + if (!strCmp(entity.kind, 'component')) { return false; } const componentEntity = entity as ComponentEntity; @@ -36,6 +44,10 @@ export function isComponentType(type: string) { }; } +/** + * For use in EntitySwitch.Case. Matches if the entity is in a given namespace. + * @public + */ export function isNamespace(namespace: string) { - return (entity: Entity) => strCmp(entity?.metadata?.namespace, namespace); + return (entity: Entity) => strCmp(entity.metadata?.namespace, namespace); } diff --git a/plugins/catalog/src/components/EntitySwitch/index.ts b/plugins/catalog/src/components/EntitySwitch/index.ts index bf4caf0e66..6cccf4be6e 100644 --- a/plugins/catalog/src/components/EntitySwitch/index.ts +++ b/plugins/catalog/src/components/EntitySwitch/index.ts @@ -15,4 +15,5 @@ */ export { EntitySwitch } from './EntitySwitch'; +export type { EntitySwitchProps, EntitySwitchCaseProps } from './EntitySwitch'; export { isKind, isNamespace, isComponentType } from './conditions'; diff --git a/plugins/catalog/src/components/FilteredEntityLayout/EntityListContainer.tsx b/plugins/catalog/src/components/FilteredEntityLayout/EntityListContainer.tsx index a39ef76b87..d210d551fb 100644 --- a/plugins/catalog/src/components/FilteredEntityLayout/EntityListContainer.tsx +++ b/plugins/catalog/src/components/FilteredEntityLayout/EntityListContainer.tsx @@ -15,10 +15,13 @@ */ import { Grid } from '@material-ui/core'; -import React, { PropsWithChildren } from 'react'; +import React from 'react'; -export const EntityListContainer = ({ children }: PropsWithChildren<{}>) => ( - - {children} - -); +/** @public */ +export function EntityListContainer(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ); +} diff --git a/plugins/catalog/src/components/FilteredEntityLayout/FilterContainer.tsx b/plugins/catalog/src/components/FilteredEntityLayout/FilterContainer.tsx index 683dbf8b40..4c0d17599d 100644 --- a/plugins/catalog/src/components/FilteredEntityLayout/FilterContainer.tsx +++ b/plugins/catalog/src/components/FilteredEntityLayout/FilterContainer.tsx @@ -25,9 +25,10 @@ import { useTheme, } from '@material-ui/core'; import FilterListIcon from '@material-ui/icons/FilterList'; -import React, { useState, PropsWithChildren } from 'react'; +import React, { useState } from 'react'; -export const FilterContainer = ({ children }: PropsWithChildren<{}>) => { +/** @public */ +export function FilterContainer(props: { children: React.ReactNode }) { const isMidSizeScreen = useMediaQuery(theme => theme.breakpoints.down('md'), ); @@ -59,13 +60,13 @@ export const FilterContainer = ({ children }: PropsWithChildren<{}>) => { > Filters - {children} + {props.children} ) : ( - {children} + {props.children} ); -}; +} diff --git a/plugins/catalog/src/components/FilteredEntityLayout/FilteredEntityLayout.tsx b/plugins/catalog/src/components/FilteredEntityLayout/FilteredEntityLayout.tsx index 628ad9fee6..8e595fe5cd 100644 --- a/plugins/catalog/src/components/FilteredEntityLayout/FilteredEntityLayout.tsx +++ b/plugins/catalog/src/components/FilteredEntityLayout/FilteredEntityLayout.tsx @@ -15,10 +15,13 @@ */ import { Grid } from '@material-ui/core'; -import React, { PropsWithChildren } from 'react'; +import React from 'react'; -export const FilteredEntityLayout = ({ children }: PropsWithChildren<{}>) => ( - - {children} - -); +/** @public */ +export function FilteredEntityLayout(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ); +} diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx index 45f7561d6e..fbb474825f 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx @@ -81,10 +81,11 @@ describe('', () => { relations: [ { target: { - kind: 'Component', + kind: 'component', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'component:my-namespace/target-name', type: RELATION_HAS_PART, }, ], diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index cefdb15a6e..5864028694 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -23,11 +23,13 @@ import { RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -type Props = { +/** @public */ +export interface HasComponentsCardProps { variant?: 'gridItem'; -}; +} -export const HasComponentsCard = ({ variant = 'gridItem' }: Props) => { +export function HasComponentsCard(props: HasComponentsCardProps) { + const { variant = 'gridItem' } = props; return ( { asRenderableEntities={asComponentEntities} /> ); -}; +} diff --git a/plugins/catalog/src/components/HasComponentsCard/index.ts b/plugins/catalog/src/components/HasComponentsCard/index.ts index 73088bd575..86038b6030 100644 --- a/plugins/catalog/src/components/HasComponentsCard/index.ts +++ b/plugins/catalog/src/components/HasComponentsCard/index.ts @@ -15,3 +15,4 @@ */ export { HasComponentsCard } from './HasComponentsCard'; +export type { HasComponentsCardProps } from './HasComponentsCard'; diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx index a01922d988..c75273e484 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx @@ -76,10 +76,11 @@ describe('', () => { relations: [ { target: { - kind: 'Resource', + kind: 'resource', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'resource:my-namespace/target-name', type: RELATION_HAS_PART, }, ], diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx index 3a57317a06..19a48aad25 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx @@ -23,11 +23,13 @@ import { resourceEntityHelpLink, } from '../RelatedEntitiesCard'; -type Props = { +/** @public */ +export interface HasResourcesCardProps { variant?: 'gridItem'; -}; +} -export const HasResourcesCard = ({ variant = 'gridItem' }: Props) => { +export function HasResourcesCard(props: HasResourcesCardProps) { + const { variant = 'gridItem' } = props; return ( { emptyHelpLink={resourceEntityHelpLink} /> ); -}; +} diff --git a/plugins/catalog/src/components/HasResourcesCard/index.ts b/plugins/catalog/src/components/HasResourcesCard/index.ts index 5357643a62..cdbbef680b 100644 --- a/plugins/catalog/src/components/HasResourcesCard/index.ts +++ b/plugins/catalog/src/components/HasResourcesCard/index.ts @@ -15,3 +15,4 @@ */ export { HasResourcesCard } from './HasResourcesCard'; +export type { HasResourcesCardProps } from './HasResourcesCard'; diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx index e517bd38ae..e021fea2f3 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx @@ -81,10 +81,11 @@ describe('', () => { relations: [ { target: { - kind: 'Component', + kind: 'component', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'component:my-namespace/target-name', type: RELATION_HAS_PART, }, ], diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx index ddeffe3fbd..0b31bbd2be 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx @@ -22,11 +22,13 @@ import { RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -type Props = { +/** @public */ +export interface HasSubcomponentsCardProps { variant?: 'gridItem'; -}; +} -export const HasSubcomponentsCard = ({ variant = 'gridItem' }: Props) => { +export function HasSubcomponentsCard(props: HasSubcomponentsCardProps) { + const { variant = 'gridItem' } = props; return ( { emptyHelpLink="https://backstage.io/docs/features/software-catalog/descriptor-format#specsubcomponentof-optional" /> ); -}; +} diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/index.ts b/plugins/catalog/src/components/HasSubcomponentsCard/index.ts index bfc74efe71..6f43967d50 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/index.ts +++ b/plugins/catalog/src/components/HasSubcomponentsCard/index.ts @@ -15,3 +15,4 @@ */ export { HasSubcomponentsCard } from './HasSubcomponentsCard'; +export type { HasSubcomponentsCardProps } from './HasSubcomponentsCard'; diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx index 050550101f..6a2e2a0b7f 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx @@ -79,10 +79,11 @@ describe('', () => { relations: [ { target: { - kind: 'System', + kind: 'system', namespace: 'my-namespace', name: 'target-name', }, + targetRef: 'system:my-namespace/target-name', type: RELATION_HAS_PART, }, ], diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx index b3e756c8dc..7ab6831e42 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx @@ -23,11 +23,13 @@ import { systemEntityHelpLink, } from '../RelatedEntitiesCard'; -type Props = { +/** @public */ +export interface HasSystemsCardProps { variant?: 'gridItem'; -}; +} -export const HasSystemsCard = ({ variant = 'gridItem' }: Props) => { +export function HasSystemsCard(props: HasSystemsCardProps) { + const { variant = 'gridItem' } = props; return ( { emptyHelpLink={systemEntityHelpLink} /> ); -}; +} diff --git a/plugins/catalog/src/components/HasSystemsCard/index.ts b/plugins/catalog/src/components/HasSystemsCard/index.ts index e69c783bb4..71fb7539b1 100644 --- a/plugins/catalog/src/components/HasSystemsCard/index.ts +++ b/plugins/catalog/src/components/HasSystemsCard/index.ts @@ -15,3 +15,4 @@ */ export { HasSystemsCard } from './HasSystemsCard'; +export type { HasSystemsCardProps } from './HasSystemsCard'; diff --git a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx index ec6ea696e2..70854c1b21 100644 --- a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx +++ b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx @@ -30,7 +30,8 @@ import { TableColumn, } from '@backstage/core-components'; -type Props = { +/** @public */ +export type RelatedEntitiesCardProps = { variant?: 'gridItem'; title: string; columns: TableColumn[]; @@ -53,7 +54,9 @@ type Props = { * * @public */ -export const RelatedEntitiesCard = (props: Props) => { +export function RelatedEntitiesCard( + props: RelatedEntitiesCardProps, +) { const { variant = 'gridItem', title, @@ -103,4 +106,4 @@ export const RelatedEntitiesCard = (props: Props) => { entities={asRenderableEntities(entities || [])} /> ); -}; +} diff --git a/plugins/catalog/src/components/RelatedEntitiesCard/index.ts b/plugins/catalog/src/components/RelatedEntitiesCard/index.ts index 8df5052de4..6012dd6553 100644 --- a/plugins/catalog/src/components/RelatedEntitiesCard/index.ts +++ b/plugins/catalog/src/components/RelatedEntitiesCard/index.ts @@ -15,4 +15,5 @@ */ export { RelatedEntitiesCard } from './RelatedEntitiesCard'; +export type { RelatedEntitiesCardProps } from './RelatedEntitiesCard'; export * from './presets'; diff --git a/plugins/catalog/src/components/Router.tsx b/plugins/catalog/src/components/Router.tsx deleted file mode 100644 index e8b00d8513..0000000000 --- a/plugins/catalog/src/components/Router.tsx +++ /dev/null @@ -1,100 +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 { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; -import { - AsyncEntityProvider, - useEntity, - useEntityFromUrl, -} from '@backstage/plugin-catalog-react'; -import { Typography } from '@material-ui/core'; -import React, { ComponentType, ReactNode } from 'react'; -import { Navigate, Route, Routes, useParams } from 'react-router'; -import { CatalogPage } from './CatalogPage'; -import { EntityNotFound } from './EntityNotFound'; -import { EntityPageLayout } from './EntityPageLayout'; -import { Content, Link } from '@backstage/core-components'; - -const DefaultEntityPage = () => ( - - - This is the default entity page. - - To override this component with your custom implementation, read - docs on{' '} - backstage.io/docs - - - } - /> - -); - -const EntityPageSwitch = ({ EntityPage }: { EntityPage: ComponentType }) => { - const { entity, loading, error } = useEntity(); - // Loading and error states - if (loading) return ; - if (error || !entity) return ; - - // Otherwise EntityPage provided from the App - // Note that EntityPage will include EntityPageLayout already - return ; -}; - -const OldEntityRouteRedirect = () => { - const { optionalNamespaceAndName, '*': rest } = useParams() as any; - const [name, namespace] = optionalNamespaceAndName.split(':').reverse(); - const namespaceLower = - namespace?.toLocaleLowerCase('en-US') ?? ENTITY_DEFAULT_NAMESPACE; - const restWithSlash = rest ? `/${rest}` : ''; - return ( - - ); -}; - -export const EntityLoader = (props: { children: ReactNode }) => ( - -); - -/** - * @deprecated Use plugin extensions instead - * */ -export const Router = ({ - EntityPage = DefaultEntityPage, -}: { - EntityPage?: ComponentType; -}) => ( - - } /> - - - - } - /> - } - /> - -); diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx index e0ec48fbed..eb6b71c47e 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx @@ -103,10 +103,11 @@ describe('', () => { relations: [ { target: { - kind: 'Domain', + kind: 'domain', namespace: 'namespace', name: 'domain', }, + targetRef: 'domain:namespace/domain', type: RELATION_PART_OF, }, ], @@ -166,6 +167,7 @@ describe('', () => { namespace: 'namespace', name: 'alongdomainthatshouldgettruncated', }, + targetRef: 'domain:namespace/alongdomainthatshouldgettruncated', type: RELATION_PART_OF, }, ], diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx index 6c17e4ee67..ec4d13a2f2 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx @@ -20,7 +20,7 @@ import { RELATION_PROVIDES_API, RELATION_PART_OF, stringifyEntityRef, - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, parseEntityRef, } from '@backstage/catalog-model'; import { @@ -92,7 +92,7 @@ function readableEntityName( ): string { return stringifyEntityRef(ref) .toLocaleLowerCase('en-US') - .replace(`:${ENTITY_DEFAULT_NAMESPACE}/`, ':') + .replace(`:${DEFAULT_NAMESPACE}/`, ':') .split(':')[1]; } @@ -180,7 +180,7 @@ export function SystemDiagramCard() { 'spec.system': [ currentSystemName, `${ - entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE + entity.metadata.namespace || DEFAULT_NAMESPACE }/${currentSystemName}`, ], }, diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index abe34c29b9..662a920f8b 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -20,26 +20,22 @@ * @packageDocumentation */ -export { CatalogClientWrapper } from './CatalogClientWrapper'; +export * from './apis'; + export * from './components/AboutCard'; export * from './components/CatalogKindHeader'; -export * from './components/CatalogResultListItem'; -export { CatalogTable } from './components/CatalogTable'; -export type { EntityRow as CatalogTableRow } from './components/CatalogTable'; -export * from './components/CatalogTable/columns'; +export * from './components/CatalogSearchResultListItem'; +export * from './components/CatalogTable'; export * from './components/EntityLayout'; export * from './components/EntityOrphanWarning'; export * from './components/EntityProcessingErrorsPanel'; -export * from './components/EntityPageLayout'; export * from './components/EntitySwitch'; export * from './components/FilteredEntityLayout'; export * from './overridableComponents'; -export { Router } from './components/Router'; export { CatalogEntityPage, CatalogIndexPage, catalogPlugin, - catalogPlugin as plugin, EntityAboutCard, EntityDependencyOfComponentsCard, EntityDependsOnComponentsCard, @@ -52,6 +48,17 @@ export { RelatedEntitiesCard, } from './plugin'; -export type { EntityLinksEmptyStateClassKey } from './components/EntityLinksCard'; +export type { DependencyOfComponentsCardProps } from './components/DependencyOfComponentsCard'; +export type { DependsOnComponentsCardProps } from './components/DependsOnComponentsCard'; +export type { DependsOnResourcesCardProps } from './components/DependsOnResourcesCard'; +export type { + EntityLinksEmptyStateClassKey, + EntityLinksCardProps, +} from './components/EntityLinksCard'; export type { SystemDiagramCardClassKey } from './components/SystemDiagramCard'; export type { DefaultCatalogPageProps } from './components/CatalogPage'; +export type { HasComponentsCardProps } from './components/HasComponentsCard'; +export type { HasResourcesCardProps } from './components/HasResourcesCard'; +export type { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; +export type { HasSystemsCardProps } from './components/HasSystemsCard'; +export type { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 2e4ebe9570..1b9083e7ca 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -15,10 +15,9 @@ */ import { CatalogClient } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, - catalogRouteRef, - DefaultStarredEntitiesApi, entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; @@ -32,7 +31,20 @@ import { fetchApiRef, storageApiRef, } from '@backstage/core-plugin-api'; +import { DefaultStarredEntitiesApi } from './apis'; +import { AboutCardProps } from './components/AboutCard'; +import { DefaultCatalogPageProps } from './components/CatalogPage'; +import { DependencyOfComponentsCardProps } from './components/DependencyOfComponentsCard'; +import { DependsOnComponentsCardProps } from './components/DependsOnComponentsCard'; +import { DependsOnResourcesCardProps } from './components/DependsOnResourcesCard'; +import { HasComponentsCardProps } from './components/HasComponentsCard'; +import { HasResourcesCardProps } from './components/HasResourcesCard'; +import { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; +import { HasSystemsCardProps } from './components/HasSystemsCard'; +import { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; +import { rootRouteRef } from './routes'; +/** @public */ export const catalogPlugin = createPlugin({ id: 'catalog', apis: [ @@ -53,7 +65,7 @@ export const catalogPlugin = createPlugin({ }), ], routes: { - catalogIndex: catalogRouteRef, + catalogIndex: rootRouteRef, catalogEntity: entityRouteRef, }, externalRoutes: { @@ -62,16 +74,19 @@ export const catalogPlugin = createPlugin({ }, }); -export const CatalogIndexPage = catalogPlugin.provide( - createRoutableExtension({ - name: 'CatalogIndexPage', - component: () => - import('./components/CatalogPage').then(m => m.CatalogPage), - mountPoint: catalogRouteRef, - }), -); +/** @public */ +export const CatalogIndexPage: (props: DefaultCatalogPageProps) => JSX.Element = + catalogPlugin.provide( + createRoutableExtension({ + name: 'CatalogIndexPage', + component: () => + import('./components/CatalogPage').then(m => m.CatalogPage), + mountPoint: rootRouteRef, + }), + ); -export const CatalogEntityPage = catalogPlugin.provide( +/** @public */ +export const CatalogEntityPage: () => JSX.Element = catalogPlugin.provide( createRoutableExtension({ name: 'CatalogEntityPage', component: () => @@ -80,15 +95,18 @@ export const CatalogEntityPage = catalogPlugin.provide( }), ); -export const EntityAboutCard = catalogPlugin.provide( - createComponentExtension({ - name: 'EntityAboutCard', - component: { - lazy: () => import('./components/AboutCard').then(m => m.AboutCard), - }, - }), -); +/** @public */ +export const EntityAboutCard: (props: AboutCardProps) => JSX.Element = + catalogPlugin.provide( + createComponentExtension({ + name: 'EntityAboutCard', + component: { + lazy: () => import('./components/AboutCard').then(m => m.AboutCard), + }, + }), + ); +/** @public */ export const EntityLinksCard = catalogPlugin.provide( createComponentExtension({ name: 'EntityLinksCard', @@ -99,17 +117,22 @@ export const EntityLinksCard = catalogPlugin.provide( }), ); -export const EntityHasSystemsCard = catalogPlugin.provide( - createComponentExtension({ - name: 'EntityHasSystemsCard', - component: { - lazy: () => - import('./components/HasSystemsCard').then(m => m.HasSystemsCard), - }, - }), -); +/** @public */ +export const EntityHasSystemsCard: (props: HasSystemsCardProps) => JSX.Element = + catalogPlugin.provide( + createComponentExtension({ + name: 'EntityHasSystemsCard', + component: { + lazy: () => + import('./components/HasSystemsCard').then(m => m.HasSystemsCard), + }, + }), + ); -export const EntityHasComponentsCard = catalogPlugin.provide( +/** @public */ +export const EntityHasComponentsCard: ( + props: HasComponentsCardProps, +) => JSX.Element = catalogPlugin.provide( createComponentExtension({ name: 'EntityHasComponentsCard', component: { @@ -119,7 +142,10 @@ export const EntityHasComponentsCard = catalogPlugin.provide( }), ); -export const EntityHasSubcomponentsCard = catalogPlugin.provide( +/** @public */ +export const EntityHasSubcomponentsCard: ( + props: HasSubcomponentsCardProps, +) => JSX.Element = catalogPlugin.provide( createComponentExtension({ name: 'EntityHasSubcomponentsCard', component: { @@ -131,7 +157,10 @@ export const EntityHasSubcomponentsCard = catalogPlugin.provide( }), ); -export const EntityHasResourcesCard = catalogPlugin.provide( +/** @public */ +export const EntityHasResourcesCard: ( + props: HasResourcesCardProps, +) => JSX.Element = catalogPlugin.provide( createComponentExtension({ name: 'EntityHasResourcesCard', component: { @@ -141,7 +170,10 @@ export const EntityHasResourcesCard = catalogPlugin.provide( }), ); -export const EntityDependsOnComponentsCard = catalogPlugin.provide( +/** @public */ +export const EntityDependsOnComponentsCard: ( + props: DependsOnComponentsCardProps, +) => JSX.Element = catalogPlugin.provide( createComponentExtension({ name: 'EntityDependsOnComponentsCard', component: { @@ -153,7 +185,10 @@ export const EntityDependsOnComponentsCard = catalogPlugin.provide( }), ); -export const EntityDependencyOfComponentsCard = catalogPlugin.provide( +/** @public */ +export const EntityDependencyOfComponentsCard: ( + props: DependencyOfComponentsCardProps, +) => JSX.Element = catalogPlugin.provide( createComponentExtension({ name: 'EntityDependencyOfComponentsCard', component: { @@ -165,7 +200,10 @@ export const EntityDependencyOfComponentsCard = catalogPlugin.provide( }), ); -export const EntityDependsOnResourcesCard = catalogPlugin.provide( +/** @public */ +export const EntityDependsOnResourcesCard: ( + props: DependsOnResourcesCardProps, +) => JSX.Element = catalogPlugin.provide( createComponentExtension({ name: 'EntityDependsOnResourcesCard', component: { @@ -177,7 +215,10 @@ export const EntityDependsOnResourcesCard = catalogPlugin.provide( }), ); -export const RelatedEntitiesCard = catalogPlugin.provide( +/** @public */ +export const RelatedEntitiesCard: ( + props: RelatedEntitiesCardProps, +) => JSX.Element = catalogPlugin.provide( createComponentExtension({ name: 'RelatedEntitiesCard', component: { diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 63d9ccfb87..5c0d195d74 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { createExternalRouteRef } from '@backstage/core-plugin-api'; +import { + createExternalRouteRef, + createRouteRef, +} from '@backstage/core-plugin-api'; export const createComponentRouteRef = createExternalRouteRef({ id: 'create-component', @@ -26,3 +29,7 @@ export const viewTechDocRouteRef = createExternalRouteRef({ optional: true, params: ['namespace', 'kind', 'name'], }); + +export const rootRouteRef = createRouteRef({ + id: 'catalog', +}); diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md index 7258ea0570..9fda781e52 100644 --- a/plugins/cicd-statistics/CHANGELOG.md +++ b/plugins/cicd-statistics/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-cicd-statistics +## 0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.1 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 0b9bce831b: Updated `@backstage/core-plugin-api` dependency. +- Updated dependencies + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index de221887c8..a9fe0b0807 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics", "description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)", - "version": "0.1.0", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,21 +24,22 @@ "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "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.4.1", - "@backstage/plugin-catalog-react": "^0.6.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", "@date-io/luxon": "^1.3.13", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.11.2", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index fcd6237d70..cc16ef1f53 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-circleci +## 0.3.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.38 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.2.37 ### Patch Changes diff --git a/plugins/circleci/api-report.md b/plugins/circleci/api-report.md index bb4334c0f9..5cdf916514 100644 --- a/plugins/circleci/api-report.md +++ b/plugins/circleci/api-report.md @@ -87,9 +87,7 @@ export const circleCIRouteRef: RouteRef; // Warning: (ae-missing-release-tag) "EntityCircleCIContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityCircleCIContent: (_props: { - entity?: Entity | undefined; -}) => JSX.Element; +export const EntityCircleCIContent: () => JSX.Element; export { GitType }; @@ -100,9 +98,8 @@ const isCircleCIAvailable: (entity: Entity) => boolean; export { isCircleCIAvailable }; export { isCircleCIAvailable as isPluginApplicableToEntity }; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: (_props: Props) => JSX.Element; +export const Router: () => JSX.Element; ``` diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 58df662e68..2005320b7d 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.37", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,21 +25,21 @@ "circleci" ], "scripts": { - "build": "backstage-cli plugin:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "clean": "backstage-cli clean", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", "diff": "backstage-cli plugin:diff", - "start": "backstage-cli plugin:serve", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack" + "start": "backstage-cli package start", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -52,10 +55,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/circleci/src/components/Router.tsx b/plugins/circleci/src/components/Router.tsx index 5aa8dd9df9..e16c91f2a0 100644 --- a/plugins/circleci/src/components/Router.tsx +++ b/plugins/circleci/src/components/Router.tsx @@ -27,12 +27,7 @@ import { MissingAnnotationEmptyState } from '@backstage/core-components'; export const isCircleCIAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[CIRCLECI_ANNOTATION]); -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; - -export const Router = (_props: Props) => { +export const Router = () => { const { entity } = useEntity(); if (!isCircleCIAvailable(entity)) { diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index 8a434df33d..f4b3c0ec6b 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-cloudbuild +## 0.3.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.36 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.2.35 ### Patch Changes diff --git a/plugins/cloudbuild/api-report.md b/plugins/cloudbuild/api-report.md index fefa4fe344..4077a8991a 100644 --- a/plugins/cloudbuild/api-report.md +++ b/plugins/cloudbuild/api-report.md @@ -149,9 +149,7 @@ export { cloudbuildPlugin as plugin }; // Warning: (ae-missing-release-tag) "EntityCloudbuildContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityCloudbuildContent: (_props: { - entity?: Entity | undefined; -}) => JSX.Element; +export const EntityCloudbuildContent: () => JSX.Element; // Warning: (ae-missing-release-tag) "EntityLatestCloudbuildRunCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -159,7 +157,6 @@ export const EntityCloudbuildContent: (_props: { export const EntityLatestCloudbuildRunCard: ({ branch, }: { - entity?: Entity | undefined; branch: string; }) => JSX.Element; @@ -169,7 +166,6 @@ export const EntityLatestCloudbuildRunCard: ({ export const EntityLatestCloudbuildsForBranchCard: ({ branch, }: { - entity?: Entity | undefined; branch: string; }) => JSX.Element; @@ -196,7 +192,6 @@ export { isCloudbuildAvailable as isPluginApplicableToEntity }; export const LatestWorkflowRunCard: ({ branch, }: { - entity?: Entity | undefined; branch: string; }) => JSX.Element; @@ -206,7 +201,6 @@ export const LatestWorkflowRunCard: ({ export const LatestWorkflowsForBranchCard: ({ branch, }: { - entity?: Entity | undefined; branch: string; }) => JSX.Element; @@ -256,11 +250,10 @@ export interface Results { buildStepOutputs: string[]; } -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: (_props: Props) => JSX.Element; +export const Router: () => JSX.Element; // Warning: (ae-missing-release-tag) "Source" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 7ed6f48f71..aca7f03b5d 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.35", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,21 +24,21 @@ "google cloud" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cloudbuild/src/components/Cards/Cards.tsx b/plugins/cloudbuild/src/components/Cards/Cards.tsx index dad1f7d38f..82e0eb1b88 100644 --- a/plugins/cloudbuild/src/components/Cards/Cards.tsx +++ b/plugins/cloudbuild/src/components/Cards/Cards.tsx @@ -16,7 +16,6 @@ import React, { useEffect } from 'react'; import { useWorkflowRuns, WorkflowRun } from '../useWorkflowRuns'; import { WorkflowRunsTable } from '../WorkflowRunsTable'; -import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { Theme, makeStyles, LinearProgress } from '@material-ui/core'; @@ -76,8 +75,6 @@ const WidgetContent = ({ export const LatestWorkflowRunCard = ({ branch = 'master', }: { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; branch: string; }) => { const { entity } = useEntity(); @@ -109,8 +106,6 @@ export const LatestWorkflowRunCard = ({ export const LatestWorkflowsForBranchCard = ({ branch = 'master', }: { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; branch: string; }) => { const { entity } = useEntity(); diff --git a/plugins/cloudbuild/src/components/Router.tsx b/plugins/cloudbuild/src/components/Router.tsx index bdeae7e8fd..e4c590bcaf 100644 --- a/plugins/cloudbuild/src/components/Router.tsx +++ b/plugins/cloudbuild/src/components/Router.tsx @@ -26,12 +26,7 @@ import { MissingAnnotationEmptyState } from '@backstage/core-components'; export const isCloudbuildAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[CLOUDBUILD_ANNOTATION]); -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; - -export const Router = (_props: Props) => { +export const Router = () => { const { entity } = useEntity(); if (!isCloudbuildAvailable(entity)) { diff --git a/plugins/code-climate/.eslintrc.js b/plugins/code-climate/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/code-climate/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md new file mode 100644 index 0000000000..596c4343b0 --- /dev/null +++ b/plugins/code-climate/CHANGELOG.md @@ -0,0 +1,15 @@ +# @backstage/plugin-code-climate + +## 0.1.0 + +### Minor Changes + +- e621d9658b: Added Code Climate plugin + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 diff --git a/plugins/code-climate/README.md b/plugins/code-climate/README.md new file mode 100644 index 0000000000..a67e70b11a --- /dev/null +++ b/plugins/code-climate/README.md @@ -0,0 +1,85 @@ +# Code Climate Plugin + +The Code Climate Plugin displays a few stats from the quality section from [Code Climate](https://codeclimate.com). + +![Code Climate Card](./docs/code-climate-card.png) + +## Getting Started + +1. Install the Code Climate Plugin: + +```bash +# From your Backstage root directory +cd packages/app +yarn add @backstage/plugin-code-climate +``` + +2. Add the `EntityCodeClimateCard` to the EntityPage: + +```jsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { EntityCodeClimateCard } from '@backstage/plugin-code-climate'; + +const overviewContent = ( + + // ... + + + + // ... + +); +``` + +3. Add the proxy config: + +```yaml +# app-config.yaml + +proxy: + '/codeclimate/api': + target: https://api.codeclimate.com/v1 + headers: + Authorization: Token token=${CODECLIMATE_TOKEN} +``` + +4. Create a new API access token (https://codeclimate.com/profile/tokens) and provide `CODECLIMATE_TOKEN` as an env variable. + +5. Add the `codeclimate.com/repo-id` annotation to your `catalog-info.yaml` file: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage + description: | + Backstage is an open-source developer portal that puts the developer experience first. + annotations: + codeclimate.com/repo-id: YOUR_REPO_ID +spec: + type: library + owner: CNCF + lifecycle: experimental +``` + +### Demo Mode + +The plugin provides a MockAPI that always returns dummy data instead of talking to the Code Climate backend. +You can add it by overriding the `codeClimateApiRef`: + +```ts +// packages/app/src/apis.ts + +import { createApiFactory } from '@backstage/core-plugin-api'; +import { + MockCodeClimateApi, + codeClimateApiRef, +} from '@backstage/plugin-code-climate'; + +export const apis = [ + // ... + + createApiFactory(codeClimateApiRef, new MockCodeClimateApi()), +]; +``` diff --git a/plugins/code-climate/api-report.md b/plugins/code-climate/api-report.md new file mode 100644 index 0000000000..11366384d3 --- /dev/null +++ b/plugins/code-climate/api-report.md @@ -0,0 +1,87 @@ +## API Report File for "@backstage/plugin-code-climate" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "CodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface CodeClimateApi { + // (undocumented) + fetchData(repoID: string): Promise; +} + +// Warning: (ae-missing-release-tag) "codeClimateApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const codeClimateApiRef: ApiRef; + +// Warning: (ae-missing-release-tag) "CodeClimateData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type CodeClimateData = { + repoID: string; + maintainability: { + letter: string; + value: string; + }; + testCoverage: { + letter: string; + value: string; + }; + numberOfCodeSmells: number; + numberOfDuplication: number; + numberOfOtherIssues: number; +}; + +// Warning: (ae-missing-release-tag) "codeClimatePlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const codeClimatePlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// Warning: (ae-missing-release-tag) "EntityCodeClimateCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityCodeClimateCard: () => JSX.Element; + +// Warning: (ae-missing-release-tag) "MockCodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class MockCodeClimateApi implements CodeClimateApi { + // (undocumented) + fetchData(): Promise; +} + +// Warning: (ae-missing-release-tag) "mockData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const mockData: CodeClimateData; + +// Warning: (ae-missing-release-tag) "ProductionCodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class ProductionCodeClimateApi implements CodeClimateApi { + constructor(discoveryApi: DiscoveryApi); + // (undocumented) + fetchAllData(options: { + apiUrl: string; + repoID: string; + snapshotID: string; + testReportID: string; + }): Promise; + // (undocumented) + fetchData(repoID: string): Promise; +} +``` diff --git a/plugins/code-climate/dev/index.tsx b/plugins/code-climate/dev/index.tsx new file mode 100644 index 0000000000..d5fc4c28e3 --- /dev/null +++ b/plugins/code-climate/dev/index.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createDevApp, EntityGridItem } from '@backstage/dev-utils'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { + EntityCodeClimateCard, + MockCodeClimateApi, + CodeClimateApi, + codeClimateApiRef, +} from '../src'; +import { CODECLIMATE_REPO_ID_ANNOTATION } from '../src/plugin'; +import { Content, Header, Page } from '@backstage/core-components'; + +const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [CODECLIMATE_REPO_ID_ANNOTATION]: name, + }, + name: name, + }, + } as Entity); + +createDevApp() + .registerApi({ + api: codeClimateApiRef, + deps: {}, + factory: () => + ({ + fetchData: async (repoID: string) => { + switch (repoID) { + case 'error': + throw new Error('Error!'); + + case 'never': + return new Promise(() => {}); + + case 'no-values': + return undefined; + + case 'with-values': + return new MockCodeClimateApi().fetchData(); + + default: + return []; + } + }, + } as CodeClimateApi), + }) + .addPage({ + title: 'Cards', + element: ( + +
+ + + + + + + + + + + + + + + + + + ), + }) + .render(); diff --git a/plugins/code-climate/docs/code-climate-card.png b/plugins/code-climate/docs/code-climate-card.png new file mode 100644 index 0000000000..b94e23eebe Binary files /dev/null and b/plugins/code-climate/docs/code-climate-card.png differ diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json new file mode 100644 index 0000000000..57b6d097ed --- /dev/null +++ b/plugins/code-climate/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-code-climate", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "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" + }, + "dependencies": { + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "humanize-duration": "^3.27.1", + "luxon": "^2.0.2", + "react-router": "6.0.0-beta.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.14.1", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/humanize-duration": "^3.27.1", + "@types/jest": "^26.0.7", + "@types/luxon": "^2.0.9", + "@types/node": "^14.14.32", + "cross-fetch": "^3.1.5", + "msw": "^0.35.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/code-climate/src/api/code-climate-api.ts b/plugins/code-climate/src/api/code-climate-api.ts new file mode 100644 index 0000000000..fb4f41ebb5 --- /dev/null +++ b/plugins/code-climate/src/api/code-climate-api.ts @@ -0,0 +1,26 @@ +/* + * 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 { CodeClimateData } from './code-climate-data'; +import { createApiRef } from '@backstage/core-plugin-api'; + +export const codeClimateApiRef = createApiRef({ + id: 'plugin.code-climate.service', +}); + +export interface CodeClimateApi { + fetchData(repoID: string): Promise; +} diff --git a/plugins/code-climate/src/api/code-climate-data.ts b/plugins/code-climate/src/api/code-climate-data.ts new file mode 100644 index 0000000000..9271905157 --- /dev/null +++ b/plugins/code-climate/src/api/code-climate-data.ts @@ -0,0 +1,184 @@ +/* + * 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 type CodeClimateRepoData = { + data: { + id: string; + type: string; + attributes: { + analysis_version: number; + badge_token: string; + branch: string; + created_at: string; + delegated_config_repo_id: string; + diff_coverage_enforced: boolean; + diff_coverage_threshold: number; + enable_notifications: boolean; + github_slug: string; + human_name: string; + last_activity_at: string; + test_reporter_id: string | null; + total_coverage_enforced: boolean; + vcs_database_id: string; + vcs_host: string; + score: string | null; + }; + relationships: { + latest_default_branch_snapshot: { + data: { id: string; type: string }; + }; + latest_default_branch_test_report: { + data: { id: string; type: string }; + }; + account: { + data: { id: string; type: string }; + }; + }; + links: { + self: string; + services: string; + web_coverage: string; + web_issues: string; + maintainability_badge: string; + test_coverage_badge: string; + }; + meta: { permissions: { admin: boolean } }; + }; +}; + +export type CodeClimateMaintainabilityData = { + data: { + id: string; + type: string; + attributes: { + commit_sha: string; + committed_at: string; + created_at: string; + lines_of_code: number; + ratings: [ + { + path: string; + letter: string; + measure: { + value: number; + unit: string; + meta: { + remediation_time: { value: number; unit: string }; + implementation_time: { + value: number; + unit: string; + }; + }; + }; + pillar: string; + }, + ]; + gpa: string | null; + worker_version: number; + }; + meta: { + issues_count: number; + measures: { + remediation: { value: number; unit: string }; + technical_debt_ratio: { + value: number; + unit: string; + meta: { + remediation_time: { value: number; unit: string }; + implementation_time: { + value: number; + unit: string; + }; + }; + }; + }; + }; + }; +}; + +export type CodeClimateTestCoverageData = { + data: { + id: string; + type: string; + attributes: { + branch: string; + commit_sha: string; + committed_at: string; + covered_percent: number; + lines_of_code: number; + rating: { + path: string; + letter: string; + measure: { value: number; unit: string }; + pillar: string; + }; + received_at: string; + state: string; + }; + }; +}; + +export type CodeClimateIssuesData = { + data: [ + { + id: string; + type: string; + attributes: { + categories: string[]; + check_name: string; + constant_name: string; + content: { body: string }; + description: string; + engine_name: string; + fingerprint: string; + location: { + path: string; + end_line: number; + start_line: number; + }; + other_locations: string[]; + remediation_points: number; + severity: string; + }; + meta: { permissions: { manageable: boolean } }; + }, + ]; + links: { + self: string; + next: string; + last: string; + }; + meta: { current_page: number; total_pages: number; total_count: number }; +}; + +export type CodeClimateData = { + repoID: string; + maintainability: { + letter: string; + value: string; + }; + testCoverage: { + letter: string; + value: string; + }; + numberOfCodeSmells: number; + numberOfDuplication: number; + numberOfOtherIssues: number; +}; + +export type CodeClimateApiError = { + detail: string; +}; diff --git a/plugins/code-climate/src/api/index.ts b/plugins/code-climate/src/api/index.ts new file mode 100644 index 0000000000..60fc079698 --- /dev/null +++ b/plugins/code-climate/src/api/index.ts @@ -0,0 +1,21 @@ +/* + * 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 * from './mock'; +export type { CodeClimateApi } from './code-climate-api'; +export { codeClimateApiRef } from './code-climate-api'; +export type { CodeClimateData } from './code-climate-data'; +export { ProductionCodeClimateApi } from './production-api'; diff --git a/plugins/code-climate/src/api/mock/code-climate-maintainability-mock.json b/plugins/code-climate/src/api/mock/code-climate-maintainability-mock.json new file mode 100644 index 0000000000..46a7905e4b --- /dev/null +++ b/plugins/code-climate/src/api/mock/code-climate-maintainability-mock.json @@ -0,0 +1,49 @@ +{ + "data": { + "id": "6b8cc37a64b741dd9d516119", + "type": "snapshots", + "attributes": { + "commit_sha": "d670460b4b4aece5915caf5c68d12f560a9fe3e4", + "committed_at": "2022-01-14T15:17:29.311Z", + "created_at": "2022-01-31T10:07:40.415Z", + "lines_of_code": 8854, + "ratings": [ + { + "path": "/", + "letter": "B", + "measure": { + "value": 5.264879308188034, + "unit": "percent", + "meta": { + "remediation_time": { "value": 7186.52, "unit": "minute" }, + "implementation_time": { + "value": 136499.2353922225, + "unit": "minute" + } + } + }, + "pillar": "Maintainability" + } + ], + "gpa": null, + "worker_version": 69440 + }, + "meta": { + "issues_count": 127, + "measures": { + "remediation": { "value": 7186.52, "unit": "minute" }, + "technical_debt_ratio": { + "value": 5.264879308188034, + "unit": "percent", + "meta": { + "remediation_time": { "value": 7186.52, "unit": "minute" }, + "implementation_time": { + "value": 136499.2353922225, + "unit": "minute" + } + } + } + } + } + } +} diff --git a/plugins/code-climate/src/api/mock/code-climate-test-coverage-mock.json b/plugins/code-climate/src/api/mock/code-climate-test-coverage-mock.json new file mode 100644 index 0000000000..203dd81776 --- /dev/null +++ b/plugins/code-climate/src/api/mock/code-climate-test-coverage-mock.json @@ -0,0 +1,21 @@ +{ + "data": { + "id": "6b8cc37a64b741dd9d516119", + "type": "test_reports", + "attributes": { + "branch": "master", + "commit_sha": "d670460b4b4aece5915caf5c68d12f560a9fe3e4", + "committed_at": "2022-01-14T15:17:26.000Z", + "covered_percent": 88.41059602649007, + "lines_of_code": 8854, + "rating": { + "path": "/", + "letter": "B", + "measure": { "value": 88.41059602649007, "unit": "percent" }, + "pillar": "Test Coverage" + }, + "received_at": "2022-01-14T15:25:38.352Z", + "state": "done" + } + } +} diff --git a/plugins/code-climate/src/api/mock/index.ts b/plugins/code-climate/src/api/mock/index.ts new file mode 100644 index 0000000000..56ba9c7974 --- /dev/null +++ b/plugins/code-climate/src/api/mock/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { mockData } from './mock-api'; +export { MockCodeClimateApi } from './mock-api'; diff --git a/plugins/code-climate/src/api/mock/mock-api.ts b/plugins/code-climate/src/api/mock/mock-api.ts new file mode 100644 index 0000000000..74e4d83027 --- /dev/null +++ b/plugins/code-climate/src/api/mock/mock-api.ts @@ -0,0 +1,56 @@ +/* + * 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 { CodeClimateData } from '../code-climate-data'; +import { CodeClimateApi } from '../code-climate-api'; +import maintainabilityMock from './code-climate-maintainability-mock.json'; +import testCoverageMock from './code-climate-test-coverage-mock.json'; +import { Duration } from 'luxon'; +import humanizeDuration from 'humanize-duration'; + +const maintainabilityData = maintainabilityMock.data.attributes.ratings[0]; +const testCoverageData = testCoverageMock.data.attributes.rating; + +const maintainabilityValue: any = {}; +maintainabilityValue[ + maintainabilityData.measure.meta.implementation_time.unit +] = maintainabilityData.measure.meta.implementation_time.value.toFixed(); + +export const mockData: CodeClimateData = { + repoID: '6b8cc37a64b741dd9d516119', + maintainability: { + letter: maintainabilityData.letter, + value: humanizeDuration( + Duration.fromObject(maintainabilityValue).toMillis(), + { largest: 1 }, + ), + }, + testCoverage: { + letter: testCoverageData.letter, + value: testCoverageData.measure.value.toFixed(), + }, + numberOfCodeSmells: 97, + numberOfDuplication: 49, + numberOfOtherIssues: 26, +}; + +export class MockCodeClimateApi implements CodeClimateApi { + fetchData(): Promise { + return new Promise(resolve => { + setTimeout(() => resolve(mockData), 800); + }); + } +} diff --git a/plugins/code-climate/src/api/production-api.ts b/plugins/code-climate/src/api/production-api.ts new file mode 100644 index 0000000000..665d7e9f63 --- /dev/null +++ b/plugins/code-climate/src/api/production-api.ts @@ -0,0 +1,160 @@ +/* + * 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 { + CodeClimateData, + CodeClimateRepoData, + CodeClimateMaintainabilityData, + CodeClimateTestCoverageData, + CodeClimateIssuesData, +} from './code-climate-data'; +import { CodeClimateApi } from './code-climate-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { Duration } from 'luxon'; +import humanizeDuration from 'humanize-duration'; + +const pageQuery = encodeURIComponent('page[size]'); +const statusFilter = encodeURIComponent('filter[status][$in][]'); +const categoriesFilter = encodeURIComponent('filter[categories][$in][]'); +const basicIssuesOptions = `${pageQuery}=1&${statusFilter}=open&${statusFilter}=confirmed`; +const codeSmellsQuery = `${basicIssuesOptions}&${categoriesFilter}=Complexity`; +const duplicationQuery = `${basicIssuesOptions}&${categoriesFilter}=Duplication`; +const otherIssuesQuery = `${basicIssuesOptions}&${categoriesFilter}=Bug%20Risk`; + +export class ProductionCodeClimateApi implements CodeClimateApi { + constructor(private readonly discoveryApi: DiscoveryApi) {} + + async fetchAllData(options: { + apiUrl: string; + repoID: string; + snapshotID: string; + testReportID: string; + }): Promise { + const { apiUrl, repoID, snapshotID, testReportID } = options; + + const [ + maintainabilityResponse, + testCoverageResponse, + codeSmellsResponse, + duplicationResponse, + otherIssuesResponse, + ] = await Promise.all([ + await fetch(`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}`), + await fetch(`${apiUrl}/repos/${repoID}/test_reports/${testReportID}`), + await fetch( + `${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${codeSmellsQuery}`, + ), + await fetch( + `${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${duplicationQuery}`, + ), + await fetch( + `${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${otherIssuesQuery}`, + ), + ]); + + if ( + !maintainabilityResponse.ok || + !testCoverageResponse.ok || + !codeSmellsResponse.ok || + !duplicationResponse.ok || + !otherIssuesResponse.ok + ) { + throw new Error('Failed fetching Code Climate info'); + } + + const maintainabilityData = ( + (await maintainabilityResponse.json()) as CodeClimateMaintainabilityData + ).data.attributes.ratings[0]; + const testCoverageData = ( + (await testCoverageResponse.json()) as CodeClimateTestCoverageData + ).data.attributes.rating; + const codeSmellsData = ( + (await codeSmellsResponse.json()) as CodeClimateIssuesData + ).meta.total_count; + const duplicationData = ( + (await duplicationResponse.json()) as CodeClimateIssuesData + ).meta.total_count; + const otherIssuesData = ( + (await otherIssuesResponse.json()) as CodeClimateIssuesData + ).meta.total_count; + + return [ + maintainabilityData, + testCoverageData, + codeSmellsData, + duplicationData, + otherIssuesData, + ]; + } + + async fetchData(repoID: string): Promise { + if (!repoID) { + throw new Error('No Repo id found'); + } + + const apiUrl = `${await this.discoveryApi.getBaseUrl( + 'proxy', + )}/codeclimate/api`; + + const repoResponse = await fetch(`${apiUrl}/repos/${repoID}`); + + if (!repoResponse.ok) { + throw new Error('Failed fetching Code Climate info'); + } + + const repoData = ((await repoResponse.json()) as CodeClimateRepoData).data; + const snapshotID = + repoData.relationships.latest_default_branch_snapshot.data.id; + const testReportID = + repoData.relationships.latest_default_branch_test_report.data.id; + + const [ + maintainabilityData, + testCoverageData, + codeSmellsData, + duplicationData, + otherIssuesData, + ] = await this.fetchAllData({ + apiUrl, + repoID, + snapshotID, + testReportID, + }); + + const maintainabilityValue: any = {}; + maintainabilityValue[ + maintainabilityData.measure.meta.implementation_time.unit + ] = maintainabilityData.measure.meta.implementation_time.value.toFixed(); + + return { + repoID, + maintainability: { + letter: maintainabilityData.letter, + value: humanizeDuration( + Duration.fromObject(maintainabilityValue).toMillis(), + { largest: 1 }, + ), + }, + testCoverage: { + letter: testCoverageData.letter, + value: testCoverageData.measure.value.toFixed(), + }, + numberOfCodeSmells: codeSmellsData, + numberOfDuplication: duplicationData, + numberOfOtherIssues: otherIssuesData, + }; + } +} diff --git a/plugins/code-climate/src/components/CodeClimateCard/CodeClimateCard.tsx b/plugins/code-climate/src/components/CodeClimateCard/CodeClimateCard.tsx new file mode 100644 index 0000000000..82af326690 --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateCard/CodeClimateCard.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { InfoCard } from '@backstage/core-components'; +import { CodeClimateCardContents } from '../CodeClimateCardContents'; + +export const CodeClimateCard = () => ( + + + +); diff --git a/plugins/catalog/src/components/EntityPageLayout/index.ts b/plugins/code-climate/src/components/CodeClimateCard/index.ts similarity index 91% rename from plugins/catalog/src/components/EntityPageLayout/index.ts rename to plugins/code-climate/src/components/CodeClimateCard/index.ts index 549d3d8454..51eaaf71af 100644 --- a/plugins/catalog/src/components/EntityPageLayout/index.ts +++ b/plugins/code-climate/src/components/CodeClimateCard/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityPageLayout } from './EntityPageLayout'; + +export { CodeClimateCard } from './CodeClimateCard'; diff --git a/plugins/code-climate/src/components/CodeClimateCardContents/CodeClimateCardContents.tsx b/plugins/code-climate/src/components/CodeClimateCardContents/CodeClimateCardContents.tsx new file mode 100644 index 0000000000..c9dc1894fc --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateCardContents/CodeClimateCardContents.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { codeClimateApiRef } from '../../api'; +import { CodeClimateTable } from '../CodeClimateTable'; +import { CODECLIMATE_REPO_ID_ANNOTATION } from '../../plugin'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { + EmptyState, + ErrorPanel, + MissingAnnotationEmptyState, + Progress, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; + +export const CodeClimateCardContents = () => { + const { entity } = useEntity(); + const codeClimateApi = useApi(codeClimateApiRef); + + const repoID = + entity?.metadata.annotations?.[CODECLIMATE_REPO_ID_ANNOTATION] ?? ''; + + const { loading, value, error } = useAsync( + () => codeClimateApi.fetchData(repoID), + [codeClimateApi, repoID], + ); + + if (loading) { + return ; + } else if (!repoID) { + return ( + + ); + } else if (error) { + return ; + } else if (!value) { + return ( + + ); + } + + return ; +}; diff --git a/plugins/code-climate/src/components/CodeClimateCardContents/index.ts b/plugins/code-climate/src/components/CodeClimateCardContents/index.ts new file mode 100644 index 0000000000..29f59be2a3 --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateCardContents/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { CodeClimateCardContents } from './CodeClimateCardContents'; diff --git a/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.test.tsx b/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.test.tsx new file mode 100644 index 0000000000..a2ced6f5a2 --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { CodeClimateTable } from './CodeClimateTable'; +import { mockData } from '../../api/mock'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; + +describe('CodeClimateTable', () => { + it('should render values in a table', async () => { + const table = await render( + + + , + ); + expect(await table.findByText('3 months')).toBeInTheDocument(); + expect(await table.findByText('88%')).toBeInTheDocument(); + expect(await table.findByText('97')).toBeInTheDocument(); + expect(await table.findByText('49')).toBeInTheDocument(); + expect(await table.findByText('26')).toBeInTheDocument(); + }); +}); diff --git a/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.tsx b/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.tsx new file mode 100644 index 0000000000..e8e204ff9a --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.tsx @@ -0,0 +1,212 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { CodeClimateData } from '../../api'; +import { Link } from '@backstage/core-components'; +import { Box, makeStyles, Typography } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +const letterStyle = { + color: 'white', + border: 0, + borderRadius: '3px', + fontSize: '40px', + padding: '5px 20px', +}; + +const fontSize = { + fontSize: '25px', +}; + +const letterColor = (letter: string) => { + if (letter === 'A') { + return '#45d298'; + } else if (letter === 'B') { + return '#a5d86e'; + } else if (letter === 'C') { + return '#f1ce0c'; + } else if (letter === 'D') { + return '#f29141'; + } else if (letter === 'F') { + return '#df5869'; + } + + return '#45d298'; +}; + +const useStyles = makeStyles< + BackstageTheme, + { + maintainabilityLetter: string; + testCoverageLetter: string; + } +>({ + spaceAround: { + display: 'flex', + justifyContent: 'space-around', + }, + spaceBetween: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, + maintainabilityLetterColor: { + ...letterStyle, + backgroundColor: props => letterColor(props.maintainabilityLetter), + }, + testCoverageLetterColor: { + ...letterStyle, + backgroundColor: props => letterColor(props.testCoverageLetter), + }, + fontSize: { + ...fontSize, + }, + letterDetails: { + ...fontSize, + paddingLeft: '10px', + }, + paddingSides20: { + padding: '0px 20px', + }, +}); + +export const CodeClimateTable = ({ + codeClimateData, +}: { + codeClimateData: CodeClimateData; +}) => { + const { + repoID, + maintainability: { + letter: maintainabilityLetter, + value: maintainabilityValue, + }, + testCoverage: { letter: testCoverageLetter, value: testCoverageValue }, + numberOfCodeSmells, + numberOfDuplication, + numberOfOtherIssues, + } = codeClimateData; + + const classes = useStyles({ maintainabilityLetter, testCoverageLetter }); + + if (!codeClimateData) { + return null; + } + + return ( + <> +
+
+ + Maintainability + +
+ + {maintainabilityLetter} + + + + {maintainabilityValue} + + +
+
+
+ + Test Coverage + +
+ + {testCoverageLetter} + + + + {testCoverageValue}% + + +
+
+
+ +
+ + Code Smells: + + + + {numberOfCodeSmells} + + +
+ + + Duplication: + + + + {numberOfDuplication} + + + +
+ + Other Issues: + + + + {numberOfOtherIssues} + + +
+
+ + ); +}; diff --git a/plugins/code-climate/src/components/CodeClimateTable/index.ts b/plugins/code-climate/src/components/CodeClimateTable/index.ts new file mode 100644 index 0000000000..bdf79bb94b --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { CodeClimateTable } from './CodeClimateTable'; diff --git a/plugins/code-climate/src/index.ts b/plugins/code-climate/src/index.ts new file mode 100644 index 0000000000..b6ef174b8f --- /dev/null +++ b/plugins/code-climate/src/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * A Backstage plugin that integrates towards Code Climate + * + * @packageDocumentation + */ + +export * from './api'; +export { codeClimatePlugin, EntityCodeClimateCard } from './plugin'; diff --git a/plugins/code-climate/src/plugin.test.ts b/plugins/code-climate/src/plugin.test.ts new file mode 100644 index 0000000000..ea791f2c87 --- /dev/null +++ b/plugins/code-climate/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { codeClimatePlugin } from './plugin'; + +describe('codeclimate', () => { + it('should export plugin', () => { + expect(codeClimatePlugin).toBeDefined(); + }); +}); diff --git a/plugins/code-climate/src/plugin.ts b/plugins/code-climate/src/plugin.ts new file mode 100644 index 0000000000..dcf86f1e19 --- /dev/null +++ b/plugins/code-climate/src/plugin.ts @@ -0,0 +1,58 @@ +/* + * 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 { ProductionCodeClimateApi, codeClimateApiRef } from './api'; +import { + createApiFactory, + createPlugin, + createRouteRef, + discoveryApiRef, + identityApiRef, + createComponentExtension, +} from '@backstage/core-plugin-api'; + +export const CODECLIMATE_REPO_ID_ANNOTATION = 'codeclimate.com/repo-id'; + +export const rootRouteRef = createRouteRef({ + id: 'code-climate', +}); + +export const codeClimatePlugin = createPlugin({ + id: 'code-climate', + apis: [ + createApiFactory({ + api: codeClimateApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ discoveryApi }) => new ProductionCodeClimateApi(discoveryApi), + }), + ], + routes: { + root: rootRouteRef, + }, +}); + +export const EntityCodeClimateCard = codeClimatePlugin.provide( + createComponentExtension({ + name: 'EntityCodeClimateCard', + component: { + lazy: () => + import('./components/CodeClimateCard').then(m => m.CodeClimateCard), + }, + }), +); diff --git a/plugins/code-climate/src/setupTests.ts b/plugins/code-climate/src/setupTests.ts new file mode 100644 index 0000000000..43d9e09713 --- /dev/null +++ b/plugins/code-climate/src/setupTests.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 5e0bbb0c0f..d5442a0d6c 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.25 + +### Patch Changes + +- 67a7c02d26: Remove usages of `EntityRef` and `parseEntityName` from `@backstage/catalog-model` +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/integration@0.7.5 + +## 0.1.24 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-client@0.7.1 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + +## 0.1.23 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/catalog-client@0.7.0 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + ## 0.1.22 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index c5c77f9873..7ce90f4165 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.22", + "version": "0.1.25", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,22 +10,25 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/backend-common": "^0.11.0", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -36,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts index 580d560bae..9235f3a8fd 100644 --- a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts @@ -15,7 +15,7 @@ */ import { resolvePackagePath } from '@backstage/backend-common'; import { NotFoundError } from '@backstage/errors'; -import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model'; +import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; import { aggregateCoverage } from './CoverageUtils'; @@ -103,7 +103,7 @@ export class CodeCoverageDatabase implements CodeCoverageStore { .map(r => JSON.parse(r.coverage)) .map(c => aggregateCoverage(c)); - const entityName = parseEntityName(entity); + const entityName = parseEntityRef(entity); return { entity: { diff --git a/plugins/code-coverage-backend/src/service/router.ts b/plugins/code-coverage-backend/src/service/router.ts index 5f136cc8e9..4922f5c887 100644 --- a/plugins/code-coverage-backend/src/service/router.ts +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -34,9 +34,8 @@ import { Cobertura } from './converter/cobertura'; import { Jacoco } from './converter/jacoco'; import { Converter } from './converter'; import { - EntityRef, getEntitySourceLocation, - parseEntityName, + parseEntityRef, } from '@backstage/catalog-model'; export interface RouterOptions { @@ -78,7 +77,7 @@ export const makeRouter = async ( */ router.get('/report', async (req, res) => { const { entity } = req.query; - const entityName = parseEntityName(entity as EntityRef); + const entityName = parseEntityRef(entity as string); const entityLookup = await catalogApi.getEntityByName(entityName); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); @@ -101,7 +100,7 @@ export const makeRouter = async ( */ router.get('/history', async (req, res) => { const { entity } = req.query; - const entityName = parseEntityName(entity as EntityRef); + const entityName = parseEntityRef(entity as string); const entityLookup = await catalogApi.getEntityByName(entityName); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); @@ -120,7 +119,7 @@ export const makeRouter = async ( */ router.get('/file-content', async (req, res) => { const { entity, path } = req.query; - const entityName = parseEntityName(entity as EntityRef); + const entityName = parseEntityRef(entity as string); const entityLookup = await catalogApi.getEntityByName(entityName); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); @@ -172,7 +171,7 @@ export const makeRouter = async ( */ router.post('/report', async (req, res) => { const { entity, coverageType } = req.query; - const entityName = parseEntityName(entity as EntityRef); + const entityName = parseEntityRef(entity as string); const entityLookup = await catalogApi.getEntityByName(entityName); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index 2fc3acda84..aca8a8ba26 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-code-coverage +## 0.1.27 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.26 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + ## 0.1.25 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 49f092bf25..ba6d792aba 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.25", + "version": "0.1.27", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,24 +10,27 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,10 +46,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index a8b6f14292..83868bd498 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-config-schema +## 0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.22 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + - @backstage/types@0.1.2 + ## 0.1.21 ### Patch Changes diff --git a/plugins/config-schema/dev/example-schema.json b/plugins/config-schema/dev/example-schema.json index ac5c2ad347..be297aaa8d 100644 --- a/plugins/config-schema/dev/example-schema.json +++ b/plugins/config-schema/dev/example-schema.json @@ -746,36 +746,6 @@ "description": "List of processor-specific options and attributes", "type": "object", "properties": { - "githubOrg": { - "description": "GithubOrgReaderProcessor configuration", - "type": "object", - "required": ["providers"], - "properties": { - "providers": { - "description": "The configuration parameters for each single GitHub org provider.", - "type": "array", - "items": { - "type": "object", - "required": ["target"], - "properties": { - "target": { - "description": "The prefix of the target that this matches on, e.g.\n\"https://github.com\", with no trailing slash.", - "type": "string" - }, - "apiBaseUrl": { - "description": "The base URL of the API of this provider, e.g.\n\"https://api.github.com\", with no trailing slash.\n\nMay be omitted specifically for GitHub; then it will be deduced.", - "type": "string" - }, - "token": { - "description": "The authorization token to use for requests to this provider.\n\nIf no token is specified, anonymous access is used.", - "visibility": "secret", - "type": "string" - } - } - } - } - } - }, "ldapOrg": { "description": "LdapOrgReaderProcessor configuration", "type": "object", diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 46e030dbc8..e52654b506 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.21", + "version": "0.1.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,23 +10,26 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/theme": "^0.2.14", - "@backstage/types": "^0.1.1", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/theme": "^0.2.15", + "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -38,10 +41,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index e1f3caf8ec..1520f476b6 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-cost-insights +## 0.11.22 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.11.21 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + ## 0.11.20 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 45178cb475..64613102cc 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.20", + "version": "0.11.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,21 +24,21 @@ "backstage" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -57,10 +60,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/src/hooks/useGroups.tsx b/plugins/cost-insights/src/hooks/useGroups.tsx index 6d20f435b3..de0e253763 100644 --- a/plugins/cost-insights/src/hooks/useGroups.tsx +++ b/plugins/cost-insights/src/hooks/useGroups.tsx @@ -26,10 +26,7 @@ import { MapLoadingToProps, useLoading } from './useLoading'; import { Group, Maybe } from '../types'; import { DefaultLoadingAction } from '../utils/loading'; import { useApi, identityApiRef } from '@backstage/core-plugin-api'; -import { - ENTITY_DEFAULT_NAMESPACE, - parseEntityRef, -} from '@backstage/catalog-model'; +import { DEFAULT_NAMESPACE, parseEntityRef } from '@backstage/catalog-model'; type GroupsProviderLoadingProps = { dispatchLoadingGroups: (isLoading: boolean) => void; @@ -66,7 +63,7 @@ export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => { const { userEntityRef } = await identityApi.getBackstageIdentity(); const { name: userId } = parseEntityRef(userEntityRef, { defaultKind: 'User', - defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + defaultNamespace: DEFAULT_NAMESPACE, }); const g = await client.getUserGroups(userId); setGroups(g); diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md index b546ab7408..9c8e4da88a 100644 --- a/plugins/explore-react/CHANGELOG.md +++ b/plugins/explore-react/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-explore-react +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.7.0 + +## 0.0.12 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-plugin-api@0.6.1 + ## 0.0.11 ### Patch Changes diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 3da9de5e08..468989d414 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore-react", "description": "A frontend library for Backstage plugins that want to interact with the explore plugin", - "version": "0.0.11", + "version": "0.0.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,20 +24,21 @@ "backstage" ], "scripts": { - "build": "backstage-cli plugin:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "clean": "backstage-cli clean", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-plugin-api": "^0.6.0" + "@backstage/core-plugin-api": "^0.7.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", - "@backstage/dev-utils": "^0.2.21-next.0", - "@backstage/test-utils": "^0.2.4-next.0", + "@backstage/cli": "^0.14.1", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index d417976d1c..319197565d 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-explore +## 0.3.31 + +### Patch Changes + +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/plugin-explore-react@0.0.13 + +## 0.3.30 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/plugin-explore-react@0.0.12 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.3.29 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 7cb22f5782..5eeb8e2a7c 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.29", + "version": "0.3.31", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,22 +24,22 @@ "backstage" ], "scripts": { - "build": "backstage-cli plugin:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "clean": "backstage-cli clean", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "start": "backstage-cli plugin:serve" + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start" }, "dependencies": { - "@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-explore-react": "^0.0.11", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-explore-react": "^0.0.13", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -50,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index d10c262357..53b225daaf 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -24,14 +24,14 @@ describe('', () => { const catalogApi: jest.Mocked = { addLocation: jest.fn(_a => new Promise(() => {})), getEntities: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index e36bd4ddb9..1310d9f31e 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -25,14 +25,14 @@ describe('', () => { const catalogApi: jest.Mocked = { addLocation: jest.fn(_a => new Promise(() => {})), getEntities: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx index 36788ed1b6..23553b8b76 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -31,7 +31,7 @@ import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef, - formatEntityRefTitle, + humanizeEntityRef, getEntityRelations, } from '@backstage/plugin-catalog-react'; import { BackstageTheme } from '@backstage/theme'; @@ -193,7 +193,7 @@ export function GroupsDiagram() { kind: catalogItem.kind, name: (catalogItem as GroupEntity).spec?.profile?.displayName || - formatEntityRefTitle(catalogItem, { defaultKind: 'Group' }), + humanizeEntityRef(catalogItem, { defaultKind: 'Group' }), }); // Edge to parent diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 5d3f7c358b..255db061aa 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -25,14 +25,14 @@ describe('', () => { const catalogApi: jest.Mocked = { addLocation: jest.fn(_a => new Promise(() => {})), getEntities: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index e446663842..e553158bf4 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-firehydrant +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.16 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 3a2a893df6: chore(deps): bump `luxon` from 2.0.2 to 2.3.0 +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/theme@0.2.15 + ## 0.1.15 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 81272e2b6d..07a3938e49 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.15", + "version": "0.1.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,35 +11,38 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "luxon": "^1.27.0", + "luxon": "^2.0.2", "react-use": "^17.2.4" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index 6e42e95021..c55ebb1aea 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-fossa +## 0.2.32 + +### Patch Changes + +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.31 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.2.30 ### Patch Changes diff --git a/plugins/fossa/dev/index.tsx b/plugins/fossa/dev/index.tsx index 018d3e3acb..1ce9ec58b2 100644 --- a/plugins/fossa/dev/index.tsx +++ b/plugins/fossa/dev/index.tsx @@ -42,7 +42,8 @@ const entity = (name?: string) => relations: [ { type: RELATION_OWNED_BY, - target: { kind: 'Group', namespace: 'default', name }, + targetRef: `group:default/${name}`, + target: { kind: 'group', namespace: 'default', name }, }, ], } as Entity); diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 53e40aae96..f6ed9df07f 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.30", + "version": "0.2.32", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,22 +25,22 @@ "fossa" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -50,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index 421ff4431f..2759afd585 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -30,13 +30,13 @@ describe('', () => { addLocation: jest.fn(), getEntities: jest.fn(), getEntityByName: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), getLocationById: jest.fn(), - getOriginLocationByEntity: jest.fn(), removeEntityByUid: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), }; const fossaApi: jest.Mocked = { getFindingSummary: jest.fn(), diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.tsx index 1b53546ccd..90a36a6177 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.tsx @@ -23,7 +23,7 @@ import { catalogApiRef, EntityRefLink, EntityRefLinks, - formatEntityRefTitle, + humanizeEntityRef, getEntityRelations, } from '@backstage/plugin-catalog-react'; import { Tooltip } from '@material-ui/core'; @@ -222,10 +222,10 @@ export const FossaPage = ({ return { entity, resolved: { - name: formatEntityRefTitle(entity), + name: humanizeEntityRef(entity), ownedByRelations, ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .map(r => humanizeEntityRef(r, { defaultKind: 'group' })) .join(', '), loading: summariesLoading, details: summary, diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index 80761a4736..b23adf1239 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-gcp-projects +## 0.3.19 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + +## 0.3.18 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- e242726f78: Switches to use react-hookz in place of react-use. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/theme@0.2.15 + ## 0.3.17 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 992336300f..dc1eb7d5b0 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.17", + "version": "0.3.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,33 +24,33 @@ "google cloud" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "react-router-dom": "6.0.0-beta.0", - "react-use": "^17.2.4" + "@react-hookz/web": "^12.3.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@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/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx index 36247ac492..b900b81695 100644 --- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -27,7 +27,7 @@ import { Typography, } from '@material-ui/core'; import React from 'react'; -import useAsync from 'react-use/lib/useAsync'; +import { useAsync, useMountEffect } from '@react-hookz/web'; import { gcpApiRef } from '../../api'; import { @@ -60,19 +60,13 @@ const DetailsPage = () => { const api = useApi(gcpApiRef); const classes = useStyles(); - const { - loading, - error, - value: details, - } = useAsync( - async () => - api.getProject( - decodeURIComponent(location.search.split('projectId=')[1]), - ), - [location.search], + const [{ status, result: details, error }, { execute }] = useAsync(async () => + api.getProject(decodeURIComponent(location.search.split('projectId=')[1])), ); - if (loading) { + useMountEffect(execute); + + if (status === 'loading') { return ; } else if (error) { return ( diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index d5a57432b3..dea12f4b60 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -18,7 +18,7 @@ import { Button, LinearProgress, Tooltip, Typography } from '@material-ui/core'; import React from 'react'; -import useAsync from 'react-use/lib/useAsync'; +import { useAsync, useMountEffect } from '@react-hookz/web'; import { gcpApiRef, Project } from '../../api'; import { @@ -58,9 +58,12 @@ const labels = ( const PageContents = () => { const api = useApi(gcpApiRef); - const { loading, error, value } = useAsync(() => api.listProjects()); + const [{ status, result, error }, { execute }] = useAsync(() => + api.listProjects(), + ); + useMountEffect(execute); - if (loading) { + if (status === 'loading') { return ; } else if (error) { return ( @@ -108,7 +111,7 @@ const PageContents = () => { }, ]} data={ - value?.map((project: Project) => ({ + result?.map((project: Project) => ({ id: project.projectId, name: project.name, projectNumber: project?.projectNumber || 'Error', diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index db988dbc9b..eb6f3c6460 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-git-release-manager +## 0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration@0.7.5 + +## 0.3.12 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/integration@0.7.3 + - @backstage/theme@0.2.15 + ## 0.3.11 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 381ca0dd24..0534039d52 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.11", + "version": "0.3.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,21 +10,24 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/integration": "^0.7.2", - "@backstage/theme": "^0.2.14", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/integration": "^0.7.5", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -40,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 2399292e8d..d48aa303d0 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-github-actions +## 0.5.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration@0.7.5 + +## 0.4.36 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 7efee0f94c: Fixed an issue where an error message was being displayed on the initial component render. This was due to values owner/repo not yet being set. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/integration@0.7.3 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.4.35 ### Patch Changes diff --git a/plugins/github-actions/api-report.md b/plugins/github-actions/api-report.md index fcb3209fb9..ee3e1d679d 100644 --- a/plugins/github-actions/api-report.md +++ b/plugins/github-actions/api-report.md @@ -31,9 +31,7 @@ export enum BuildStatus { // Warning: (ae-missing-release-tag) "EntityGithubActionsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityGithubActionsContent: (_props: { - entity?: Entity | undefined; -}) => JSX.Element; +export const EntityGithubActionsContent: () => JSX.Element; // Warning: (ae-missing-release-tag) "EntityLatestGithubActionRunCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -42,7 +40,6 @@ export const EntityLatestGithubActionRunCard: ({ branch, variant, }: { - entity?: Entity | undefined; branch: string; variant?: InfoCardVariants | undefined; }) => JSX.Element; @@ -54,7 +51,6 @@ export const EntityLatestGithubActionsForBranchCard: ({ branch, variant, }: { - entity?: Entity | undefined; branch: string; variant?: InfoCardVariants | undefined; }) => JSX.Element; @@ -315,7 +311,7 @@ export type Jobs = { export const LatestWorkflowRunCard: ({ branch, variant, -}: Props_3) => JSX.Element; +}: Props_2) => JSX.Element; // Warning: (ae-missing-release-tag) "LatestWorkflowsForBranchCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -323,7 +319,7 @@ export const LatestWorkflowRunCard: ({ export const LatestWorkflowsForBranchCard: ({ branch, variant, -}: Props_3) => JSX.Element; +}: Props_2) => JSX.Element; // Warning: (ae-missing-release-tag) "RecentWorkflowRunsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -335,11 +331,10 @@ export const RecentWorkflowRunsCard: ({ variant, }: 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) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: (_props: Props_2) => JSX.Element; +export const Router: () => JSX.Element; // Warning: (ae-missing-release-tag) "Step" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index cb112fedb1..2802374a61 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.4.35", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -23,22 +26,22 @@ "github actions" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/integration": "^0.7.2", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/integration": "^0.7.5", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -52,10 +55,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-actions/src/components/Cards/Cards.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx index cb4b2190fc..61a5146bee 100644 --- a/plugins/github-actions/src/components/Cards/Cards.tsx +++ b/plugins/github-actions/src/components/Cards/Cards.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { useEntity } from '@backstage/plugin-catalog-react'; import { @@ -24,7 +23,7 @@ import { } from '@material-ui/core'; import ExternalLinkIcon from '@material-ui/icons/Launch'; import React, { useEffect } from 'react'; -import { GITHUB_ACTIONS_ANNOTATION } from '../useProjectName'; +import { GITHUB_ACTIONS_ANNOTATION } from '../getProjectNameFromEntity'; import { useWorkflowRuns, WorkflowRun } from '../useWorkflowRuns'; import { WorkflowRunsTable } from '../WorkflowRunsTable'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; @@ -122,8 +121,6 @@ export const LatestWorkflowRunCard = ({ }; type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; branch: string; variant?: InfoCardVariants; }; diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx index 0f9573e051..a987a8569f 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -75,7 +75,7 @@ describe('', () => { jest.resetAllMocks(); }); - const renderSubject = (props: RecentWorkflowRunsCardProps = { entity }) => + const renderSubject = (props: RecentWorkflowRunsCardProps = {}) => render( @@ -85,7 +85,7 @@ describe('', () => { [configApiRef, configApi], ]} > - + @@ -113,7 +113,7 @@ describe('', () => { it('requests only the required number of workflow runs', async () => { const limit = 3; - renderSubject({ entity, limit }); + renderSubject({ limit }); expect(useWorkflowRuns).toHaveBeenCalledWith( expect.objectContaining({ initialPageSize: limit }), ); @@ -128,7 +128,7 @@ describe('', () => { it('filters workflows by branch if one is specified', async () => { const branch = 'master'; - renderSubject({ entity, branch }); + renderSubject({ branch }); expect(useWorkflowRuns).toHaveBeenCalledWith( expect.objectContaining({ branch }), ); diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx index 3423bea84d..4e37a2b19b 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { useEntity } from '@backstage/plugin-catalog-react'; import React, { useEffect } from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; -import { GITHUB_ACTIONS_ANNOTATION } from '../useProjectName'; +import { GITHUB_ACTIONS_ANNOTATION } from '../getProjectNameFromEntity'; import { useWorkflowRuns } from '../useWorkflowRuns'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { Typography } from '@material-ui/core'; @@ -34,8 +33,6 @@ import { const firstLine = (message: string): string => message.split('\n')[0]; export type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; branch?: string; dense?: boolean; limit?: number; diff --git a/plugins/github-actions/src/components/Router.tsx b/plugins/github-actions/src/components/Router.tsx index ea07213c45..793293cce8 100644 --- a/plugins/github-actions/src/components/Router.tsx +++ b/plugins/github-actions/src/components/Router.tsx @@ -20,18 +20,13 @@ import { Routes, Route } from 'react-router'; import { buildRouteRef } from '../routes'; import { WorkflowRunDetails } from './WorkflowRunDetails'; import { WorkflowRunsTable } from './WorkflowRunsTable'; -import { GITHUB_ACTIONS_ANNOTATION } from './useProjectName'; +import { GITHUB_ACTIONS_ANNOTATION } from './getProjectNameFromEntity'; import { MissingAnnotationEmptyState } from '@backstage/core-components'; export const isGithubActionsAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]); -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; - -export const Router = (_props: Props) => { +export const Router = () => { const { entity } = useEntity(); if (!isGithubActionsAvailable(entity)) { diff --git a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index aebf7dfa01..989ab27800 100644 --- a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -39,7 +39,7 @@ import ExternalLinkIcon from '@material-ui/icons/Launch'; import { DateTime } from 'luxon'; import React from 'react'; import { Job, Jobs, Step } from '../../api'; -import { useProjectName } from '../useProjectName'; +import { getProjectNameFromEntity } from '../getProjectNameFromEntity'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { useWorkflowRunJobs } from './useWorkflowRunJobs'; import { useWorkflowRunsDetails } from './useWorkflowRunsDetails'; @@ -164,25 +164,25 @@ const JobsList = ({ jobs, entity }: { jobs?: Jobs; entity: Entity }) => { export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { const config = useApi(configApiRef); - const projectName = useProjectName(entity); + const projectName = getProjectNameFromEntity(entity); // TODO: Get github hostname from metadata annotation const hostname = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], )[0].host; - const [owner, repo] = projectName.value ? projectName.value.split('/') : []; + const [owner, repo] = (projectName && projectName.split('/')) || []; const details = useWorkflowRunsDetails({ hostname, owner, repo }); const jobs = useWorkflowRunJobs({ hostname, owner, repo }); - const error = projectName.error || (projectName.value && details.error); const classes = useStyles(); - if (error) { + + if (details.error && details.error.message) { return ( - Failed to load build, {error.message} + Failed to load build, {details.error.message} ); - } else if (projectName.loading || details.loading) { + } else if (details.loading) { return ; } return ( diff --git a/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts b/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts index 002d379ed1..ba0f3b6ba8 100644 --- a/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts +++ b/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts @@ -37,7 +37,7 @@ export const useWorkflowRunJobs = ({ repo, id: parseInt(id, 10), }) - : Promise.reject('No repo/owner provided'); + : Promise.reject(new Error('No repo/owner provided')); }, [repo, owner, id]); return jobs; }; diff --git a/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts b/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts index ef9b4f7ccf..10a043161e 100644 --- a/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts +++ b/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts @@ -37,7 +37,7 @@ export const useWorkflowRunsDetails = ({ repo, id: parseInt(id, 10), }) - : Promise.reject('No repo/owner provided'); + : Promise.reject(new Error('No repo/owner provided')); }, [repo, owner, id]); return details; }; diff --git a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx index 0f1670bbbc..bf66399288 100644 --- a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx +++ b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx @@ -33,7 +33,7 @@ import { import DescriptionIcon from '@material-ui/icons/Description'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import React from 'react'; -import { useProjectName } from '../useProjectName'; +import { getProjectNameFromEntity } from '../getProjectNameFromEntity'; import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs'; const useStyles = makeStyles(theme => ({ @@ -77,13 +77,13 @@ export const WorkflowRunLogs = ({ }) => { const config = useApi(configApiRef); const classes = useStyles(); - const projectName = useProjectName(entity); + const projectName = getProjectNameFromEntity(entity); // TODO: Get github hostname from metadata annotation const hostname = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], )[0].host; - const [owner, repo] = projectName.value ? projectName.value.split('/') : []; + const [owner, repo] = (projectName && projectName.split('/')) || []; const jobLogs = useDownloadWorkflowRunLogs({ hostname, owner, diff --git a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index 81fa036221..c8c42eb1bc 100644 --- a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -29,7 +29,7 @@ import { useWorkflowRuns, WorkflowRun } from '../useWorkflowRuns'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import SyncIcon from '@material-ui/icons/Sync'; import { buildRouteRef } from '../../routes'; -import { useProjectName } from '../useProjectName'; +import { getProjectNameFromEntity } from '../getProjectNameFromEntity'; import { Entity } from '@backstage/catalog-model'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; @@ -157,7 +157,7 @@ export const WorkflowRunsTable = ({ branch?: string; }) => { const config = useApi(configApiRef); - const { value: projectName, loading } = useProjectName(entity); + const projectName = getProjectNameFromEntity(entity); // TODO: Get github hostname from metadata annotation const hostname = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], @@ -172,7 +172,7 @@ export const WorkflowRunsTable = ({ }); const githubHost = hostname || 'github.com'; - const hasNoRuns = !loading && !tableProps.loading && !runs; + const hasNoRuns = !tableProps.loading && !runs; return hasNoRuns ? ( { - const { value, loading, error } = useAsync(async () => { - return entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? ''; - }); - return { value, loading, error }; -}; +export const getProjectNameFromEntity = (entity: Entity) => + entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? ''; diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts index f520b9fbbc..ab041b4970 100644 --- a/plugins/github-actions/src/index.ts +++ b/plugins/github-actions/src/index.ts @@ -35,4 +35,4 @@ export { isGithubActionsAvailable as isPluginApplicableToEntity, } from './components/Router'; export * from './components/Cards'; -export { GITHUB_ACTIONS_ANNOTATION } from './components/useProjectName'; +export { GITHUB_ACTIONS_ANNOTATION } from './components/getProjectNameFromEntity'; diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 8bcaa8287f..e2d398c7f8 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-github-deployments +## 0.1.31 + +### Patch Changes + +- 209fd128e6: The removed `'github'` catalog location type no longer triggers a GitHub integration configuration lookup. +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration@0.7.5 + - @backstage/integration-react@0.1.23 + +## 0.1.30 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 216725b434: Updated to use new names for `parseLocationRef` and `stringifyLocationRef` +- 27eccab216: Replaces use of deprecated catalog-model constants. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/integration-react@0.1.22 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.1.29 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index e4c3e4917c..20198ee40a 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-deployments", "description": "A Backstage plugin that integrates towards GitHub Deployments", - "version": "0.1.29", + "version": "0.1.31", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,25 +10,28 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@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.21", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/integration-react": "^0.1.23", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -40,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index b64d914612..48363557ff 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { parseLocationReference } from '@backstage/catalog-model'; +import { parseLocationRef } from '@backstage/catalog-model'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; @@ -27,8 +27,8 @@ const getBaseUrl = ( return 'https://api.github.com'; } - const location = parseLocationReference(host); - if (location.type !== 'github' && location.type !== 'url') { + const location = parseLocationRef(host); + if (location.type !== 'url') { return 'https://api.github.com'; } diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index dba5bbfe1c..dedd2c3082 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -254,7 +254,7 @@ describe('github-deployments', () => { entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', 'backstage.io/source-location': - 'github:https://my-github-1.com/org/repo/tree/master/', + 'url:https://my-github-1.com/org/repo/tree/master/', }; }); @@ -276,7 +276,7 @@ describe('github-deployments', () => { entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', 'backstage.io/managed-by-location': - 'github:https://my-github-2.com/org/repo/blob/master/catalog-info.yaml', + 'url:https://my-github-2.com/org/repo/blob/master/catalog-info.yaml', }; }); @@ -298,7 +298,7 @@ describe('github-deployments', () => { entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', 'backstage.io/managed-by-location': - 'github:https://my-github-3.com/org/repo/blob/master/catalog-info.yaml', + 'url:https://my-github-3.com/org/repo/blob/master/catalog-info.yaml', }; }); @@ -323,7 +323,7 @@ describe('github-deployments', () => { entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', 'backstage.io/managed-by-location': - 'github:https://my-github-unknown.com/org/repo/blob/master/catalog-info.yaml', + 'url:https://my-github-unknown.com/org/repo/blob/master/catalog-info.yaml', }; }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 901e20a899..68881617f4 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -23,8 +23,8 @@ import { } from '../Router'; import { GithubDeploymentsTable } from './GithubDeploymentsTable/GithubDeploymentsTable'; import { - LOCATION_ANNOTATION, - SOURCE_LOCATION_ANNOTATION, + ANNOTATION_LOCATION, + ANNOTATION_SOURCE_LOCATION, } from '@backstage/catalog-model'; import { @@ -88,8 +88,8 @@ export const GithubDeploymentsCard = (props: { const { last, lastStatuses, columns } = props; const { entity } = useEntity(); const [host] = [ - entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION], - entity?.metadata.annotations?.[LOCATION_ANNOTATION], + entity?.metadata.annotations?.[ANNOTATION_SOURCE_LOCATION], + entity?.metadata.annotations?.[ANNOTATION_LOCATION], ].filter(Boolean); return !isGithubDeploymentsAvailable(entity) ? ( diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index edf8bec0ae..c2fba95783 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-gitops-profiles +## 0.3.18 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + +## 0.3.17 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/theme@0.2.15 + ## 0.3.16 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 1cadbc8d65..d96e6a670d 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "description": "A Backstage plugin that helps you manage GitOps profiles", - "version": "0.3.16", + "version": "0.3.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,19 +25,19 @@ "gitops" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -45,10 +48,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md index b2e5024fea..296d7cc928 100644 --- a/plugins/gocd/CHANGELOG.md +++ b/plugins/gocd/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-gocd +## 0.1.6 + +### Patch Changes + +- 29f952c667: Updated README +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.5 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.1.4 ### Patch Changes diff --git a/plugins/gocd/README.md b/plugins/gocd/README.md index 105d93e830..d3d9c3adc8 100644 --- a/plugins/gocd/README.md +++ b/plugins/gocd/README.md @@ -4,6 +4,8 @@ Welcome to the GoCD plugin! - View recent GoCD Builds +![gocd-builds-card](./docs/gocd-plugin-screenshot.png) + ## Installation GoCD Plugin exposes an entity tab component named `EntityGoCdContent`. You can include it in the diff --git a/plugins/gocd/docs/gocd-plugin-screenshot.png b/plugins/gocd/docs/gocd-plugin-screenshot.png new file mode 100644 index 0000000000..8ddc786fa5 Binary files /dev/null and b/plugins/gocd/docs/gocd-plugin-screenshot.png differ diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json index 86802a43f2..afd6c63a91 100644 --- a/plugins/gocd/package.json +++ b/plugins/gocd/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gocd", "description": "A Backstage plugin that integrates towards GoCD", - "version": "0.1.4", + "version": "0.1.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -18,22 +21,22 @@ "directory": "plugins/gocd" }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -46,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index da7919fb5e..522c67d32c 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-graphiql +## 0.2.32 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.31 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/theme@0.2.15 + ## 0.2.30 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 185e273c3f..517d9c097d 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,13 +1,16 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.30", + "version": "0.2.32", "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://github.com/backstage/backstage/tree/master/plugins/graphiql#readme", "repository": { "type": "git", @@ -21,19 +24,19 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -45,10 +48,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphql-backend/CHANGELOG.md b/plugins/graphql-backend/CHANGELOG.md index 749ced2527..5fa09c1432 100644 --- a/plugins/graphql-backend/CHANGELOG.md +++ b/plugins/graphql-backend/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-graphql-backend +## 0.1.17 + +### Patch Changes + +- 68913af4ff: chore(deps): bump `graphql-modules` from 1.4.4 to 2.0.0 +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-catalog-graphql@0.3.4 + +## 0.1.16 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/plugin-catalog-graphql@0.3.3 + +## 0.1.15 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 0107c9aa08: chore(deps): bump `helmet` from 4.4.1 to 5.0.2 +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/plugin-catalog-graphql@0.3.2 + - @backstage/config@0.1.14 + ## 0.1.14 ### Patch Changes diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index a13a901646..d1d0f5a153 100644 --- a/plugins/graphql-backend/package.json +++ b/plugins/graphql-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphql-backend", "description": "An experimental Backstage backend plugin for GraphQL", - "version": "0.1.14", + "version": "0.1.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,33 +25,33 @@ "graphql" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config": "^0.1.13", - "@backstage/plugin-catalog-graphql": "^0.3.1", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/plugin-catalog-graphql": "^0.3.4", "@graphql-tools/schema": "^8.3.1", - "graphql-modules": "^1.0.0", + "graphql-modules": "^2.0.0", "@types/express": "^4.17.6", "apollo-server": "^3.0.0", "apollo-server-express": "^3.0.0", "express": "^4.17.1", "express-promise-router": "^4.1.0", "graphql": "^16.0.0", - "helmet": "^4.0.0", + "helmet": "^5.0.2", "reflect-metadata": "^0.1.13", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", "msw": "^0.35.0", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index d0c0a5bb44..93fe89340d 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-home +## 0.4.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/plugin-search@0.7.1 + +## 0.4.15 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 651b919dbb: Add Renderer support for the HomePageToolkit component. + + Previously `` would + result in the error `can't access property "map", props.tools is undefined`. + This change adds a context that can pass props down to the HomePageToolkit. + Also introduced is an `expanded` prop on the `ComponentAccordion` to setting + the default expanded state. See `In Accordian` story for details. + +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/plugin-search@0.7.0 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.4.14 ### Patch Changes diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index 264189ee07..6fae50151c 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -24,6 +24,7 @@ export type ClockConfig = { // @public (undocumented) export const ComponentAccordion: ({ title, + expanded, Content, Actions, Settings, @@ -31,6 +32,7 @@ export const ComponentAccordion: ({ ...childProps }: { title: string; + expanded?: boolean | undefined; Content: () => JSX.Element; Actions?: (() => JSX.Element) | undefined; Settings?: (() => JSX.Element) | undefined; diff --git a/plugins/home/package.json b/plugins/home/package.json index f57b0bb83e..3f265e136c 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.14", + "version": "0.4.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,23 +10,36 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, + "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", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-search": "^0.7.1", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/home/src/componentRenderers/ComponentAccordion.tsx b/plugins/home/src/componentRenderers/ComponentAccordion.tsx index 5cf54d7a0c..ef44f7f292 100644 --- a/plugins/home/src/componentRenderers/ComponentAccordion.tsx +++ b/plugins/home/src/componentRenderers/ComponentAccordion.tsx @@ -33,10 +33,14 @@ const useStyles = makeStyles((theme: Theme) => ({ settingsIconButton: { padding: theme.spacing(0, 1, 0, 0), }, + contentContainer: { + width: '100%', + }, })); export const ComponentAccordion = ({ title, + expanded = false, Content, Actions, Settings, @@ -44,6 +48,7 @@ export const ComponentAccordion = ({ ...childProps }: { title: string; + expanded?: boolean; Content: () => JSX.Element; Actions?: () => JSX.Element; Settings?: () => JSX.Element; @@ -51,7 +56,7 @@ export const ComponentAccordion = ({ }) => { const classes = useStyles(); const [settingsIsExpanded, setSettingsIsExpanded] = React.useState(false); - const [isExpanded, setIsExpanded] = React.useState(false); + const [isExpanded, setIsExpanded] = React.useState(expanded); const handleOpenSettings = (e: any) => { e.stopPropagation(); @@ -71,7 +76,9 @@ export const ComponentAccordion = ({ )} setIsExpanded(expanded)} + onChange={(_e: any, expandedValue: boolean) => + setIsExpanded(expandedValue) + } > }> {Settings && ( @@ -85,7 +92,7 @@ export const ComponentAccordion = ({ {title} -
+
{Actions && }
diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx index ccc36f6964..9087ed3605 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx @@ -13,40 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - renderInTestApp, - TestApiProvider, - MockStorageApi, -} from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { starredEntitiesApiRef, + MockStarredEntitiesApi, 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 mockedApi = new MockStarredEntitiesApi(); + mockedApi.toggleStarred('component:default/mock-starred-entity'); + mockedApi.toggleStarred('component:default/mock-starred-entity-2'); const { getByText } = await renderInTestApp( - + , { diff --git a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx index 2e63762369..315c0d0d16 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx @@ -22,8 +22,8 @@ import { } from '@backstage/test-utils'; import { starredEntitiesApiRef, + MockStarredEntitiesApi, entityRouteRef, - DefaultStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { Grid } from '@material-ui/core'; import React, { ComponentType } from 'react'; @@ -44,14 +44,7 @@ export default { (Story: ComponentType<{}>) => wrapInTestApp( , diff --git a/plugins/home/src/homePageComponents/Toolkit/Content.test.tsx b/plugins/home/src/homePageComponents/Toolkit/Content.test.tsx index 7fbaff08f9..1955019a4a 100644 --- a/plugins/home/src/homePageComponents/Toolkit/Content.test.tsx +++ b/plugins/home/src/homePageComponents/Toolkit/Content.test.tsx @@ -13,19 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { Content } from './Content'; +import { ContextProvider } from './Context'; describe('', () => { + const tools = [ + { label: 'tool', url: '/url', icon:
icon
}, + { label: 'tool 2', url: '/url-2', icon:
icon 2
}, + ]; + test('should render list of tools', async () => { + const { getByText } = await renderInTestApp(); + + expect(getByText('tool')).toBeInTheDocument(); + expect(getByText('tool 2')).toBeInTheDocument(); + expect(getByText('tool').closest('a')).toHaveAttribute('href', '/url'); + expect(getByText('tool 2').closest('a')).toHaveAttribute('href', '/url-2'); + }); + + test('should render list of tools using context', async () => { const { getByText } = await renderInTestApp( - icon
}, - { label: 'tool 2', url: '/url-2', icon:
icon 2
}, - ]} - />, + + + , ); expect(getByText('tool')).toBeInTheDocument(); diff --git a/plugins/home/src/homePageComponents/Toolkit/Content.tsx b/plugins/home/src/homePageComponents/Toolkit/Content.tsx index 4ee1884bd0..21affc57f1 100644 --- a/plugins/home/src/homePageComponents/Toolkit/Content.tsx +++ b/plugins/home/src/homePageComponents/Toolkit/Content.tsx @@ -22,6 +22,7 @@ import { ListItemText, } from '@material-ui/core'; import React from 'react'; +import { useToolkit, Tool } from './Context'; const useStyles = makeStyles(theme => ({ toolkit: { @@ -49,12 +50,6 @@ const useStyles = makeStyles(theme => ({ }, })); -type Tool = { - label: string; - url: string; - icon: React.ReactNode; -}; - /** * Props for Toolkit content component {@link Content}. * @@ -71,10 +66,12 @@ export type ToolkitContentProps = { */ export const Content = (props: ToolkitContentProps) => { const classes = useStyles(); + const toolkit = useToolkit(); + const tools = toolkit?.tools ?? props.tools; return ( - {props.tools.map((tool: Tool) => ( + {tools.map((tool: Tool) => ( {tool.icon} (undefined); + +export const ContextProvider = ({ + children, + tools, +}: { + children: JSX.Element; + tools: Tool[]; +}) => { + const [toolsValue, _setTools] = React.useState(tools); + + const value: ToolkitContextValue = { + tools: toolsValue, + }; + + return {children}; +}; + +export const useToolkit = () => { + const value = React.useContext(Context); + return value; +}; + +export default Context; diff --git a/plugins/home/src/homePageComponents/Toolkit/Toolkit.stories.tsx b/plugins/home/src/homePageComponents/Toolkit/Toolkit.stories.tsx index 0a8e6fffbe..cad188c5d6 100644 --- a/plugins/home/src/homePageComponents/Toolkit/Toolkit.stories.tsx +++ b/plugins/home/src/homePageComponents/Toolkit/Toolkit.stories.tsx @@ -14,11 +14,13 @@ * limitations under the License. */ -import { TemplateBackstageLogoIcon } from '../../templates'; -import { HomePageToolkit } from '../../plugin'; +import { InfoCard } from '@backstage/core-components'; import { wrapInTestApp } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; import React, { ComponentType } from 'react'; +import { ComponentAccordion } from '../../componentRenderers'; +import { HomePageToolkit } from '../../plugin'; +import { TemplateBackstageLogoIcon } from '../../templates'; export default { title: 'Plugins/Home/Components/Toolkit', @@ -38,3 +40,43 @@ export const Default = () => { ); }; + +export const InAccordian = () => { + const ExpandedComponentAccordion = (props: any) => ( + + ); + + return ( + + + , + })} + Renderer={ExpandedComponentAccordion} + /> + , + })} + Renderer={ComponentAccordion} + /> + , + })} + Renderer={ComponentAccordion} + /> + + + ); +}; diff --git a/plugins/home/src/homePageComponents/Toolkit/index.ts b/plugins/home/src/homePageComponents/Toolkit/index.ts index 98e0869825..67ea1f3c84 100644 --- a/plugins/home/src/homePageComponents/Toolkit/index.ts +++ b/plugins/home/src/homePageComponents/Toolkit/index.ts @@ -16,3 +16,4 @@ export { Content } from './Content'; export type { ToolkitContentProps } from './Content'; +export { ContextProvider } from './Context'; diff --git a/plugins/home/src/templates/DefaultTemplate.stories.tsx b/plugins/home/src/templates/DefaultTemplate.stories.tsx index ba5e1f33fd..5dc4ff61a0 100644 --- a/plugins/home/src/templates/DefaultTemplate.stories.tsx +++ b/plugins/home/src/templates/DefaultTemplate.stories.tsx @@ -25,8 +25,8 @@ import { wrapInTestApp, TestApiProvider, MockStorageApi} from '@backstage/test-u import { Content, Page, InfoCard } from '@backstage/core-components'; import { starredEntitiesApiRef, + MockStarredEntitiesApi, entityRouteRef, - DefaultStarredEntitiesApi } from '@backstage/plugin-catalog-react'; import { HomePageSearchBar, @@ -57,9 +57,7 @@ export default { apis={[ [ starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ - storageApi: mockStorageApi, - }), + new MockStarredEntitiesApi(), ], [searchApiRef, { query: () => Promise.resolve({ results: [] }) }], ]} @@ -153,4 +151,3 @@ export const DefaultTemplate = () => { ); }; - diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index 099110e39f..7e3296691f 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-ilert +## 0.1.26 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.25 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.1.24 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 4c6581553c..d2250287b0 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.1.24", + "version": "0.1.26", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,23 +10,26 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -40,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx index d58fa91108..909e4dc362 100644 --- a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx +++ b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx @@ -27,10 +27,7 @@ import { identityApiRef, } from '@backstage/core-plugin-api'; import { Progress, Link } from '@backstage/core-components'; -import { - ENTITY_DEFAULT_NAMESPACE, - parseEntityRef, -} from '@backstage/catalog-model'; +import { DEFAULT_NAMESPACE, parseEntityRef } from '@backstage/catalog-model'; export const IncidentActionsMenu = ({ incident, @@ -71,7 +68,7 @@ export const IncidentActionsMenu = ({ const { userEntityRef } = await identityApi.getBackstageIdentity(); const { name: userName } = parseEntityRef(userEntityRef, { defaultKind: 'User', - defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + defaultNamespace: DEFAULT_NAMESPACE, }); const newIncident = await ilertApi.acceptIncident(incident, userName); alertApi.post({ message: 'Incident accepted.' }); @@ -91,7 +88,7 @@ export const IncidentActionsMenu = ({ const { userEntityRef } = await identityApi.getBackstageIdentity(); const { name: userName } = parseEntityRef(userEntityRef, { defaultKind: 'User', - defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + defaultNamespace: DEFAULT_NAMESPACE, }); const newIncident = await ilertApi.resolveIncident(incident, userName); alertApi.post({ message: 'Incident resolved.' }); diff --git a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx index c8f63c6073..561047d45e 100644 --- a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx +++ b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx @@ -14,10 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { - ENTITY_DEFAULT_NAMESPACE, - parseEntityRef, -} from '@backstage/catalog-model'; +import { DEFAULT_NAMESPACE, parseEntityRef } from '@backstage/catalog-model'; import { makeStyles } from '@material-ui/core/styles'; import Alert from '@material-ui/lab/Alert'; import Button from '@material-ui/core/Button'; @@ -108,7 +105,7 @@ export const IncidentNewModal = ({ const { userEntityRef } = await identityApi.getBackstageIdentity(); const { name: userName } = parseEntityRef(userEntityRef, { defaultKind: 'User', - defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + defaultNamespace: DEFAULT_NAMESPACE, }); await ilertApi.createIncident({ integrationKey, diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 17fd3da85a..fe53f21004 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-jenkins-backend +## 0.1.16 + +### Patch Changes + +- 67a7c02d26: Remove usages of `EntityRef` and `parseEntityName` from `@backstage/catalog-model` +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + +## 0.1.15 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-client@0.7.1 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + +## 0.1.14 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 377fce4677: The `DefaultJenkinsInfoProvider.fromConfig` now requires an implementation of the `CatalogApi` rather than a `CatalogClient` instance. +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/catalog-client@0.7.0 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + ## 0.1.13 ### Patch Changes diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index b8d171f9c1..d437a5dd75 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -3,7 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { EntityName } from '@backstage/catalog-model'; import express from 'express'; @@ -21,7 +21,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { // (undocumented) static fromConfig(options: { config: Config; - catalog: CatalogClient; + catalog: CatalogApi; }): DefaultJenkinsInfoProvider; // (undocumented) getInstance(opt: { diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 52e8862fae..66cbe5ba24 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.1.13", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,21 +11,24 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "configSchema": "config.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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/catalog-client": "^0.6.0", - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -34,7 +37,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts index 496fe535e3..62299926df 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi } from '@backstage/catalog-client'; import { Entity, EntityName } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { @@ -159,9 +159,9 @@ describe('JenkinsConfig', () => { }); describe('DefaultJenkinsInfoProvider', () => { - const mockCatalog: jest.Mocked = { + const mockCatalog: jest.Mocked = { getEntityByName: jest.fn(), - } as any as jest.Mocked; + } as any as jest.Mocked; const entityRef: EntityName = { kind: 'Component', diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts index 7dbc94dacf..f822f475f1 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi } from '@backstage/catalog-client'; import { Entity, EntityName, @@ -168,12 +168,12 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { private constructor( private readonly config: JenkinsConfig, - private readonly catalog: CatalogClient, + private readonly catalog: CatalogApi, ) {} static fromConfig(options: { config: Config; - catalog: CatalogClient; + catalog: CatalogApi; }): DefaultJenkinsInfoProvider { return new DefaultJenkinsInfoProvider( JenkinsConfig.fromConfig(options.config), diff --git a/plugins/jenkins-backend/src/service/standaloneServer.ts b/plugins/jenkins-backend/src/service/standaloneServer.ts index b99bdcfd5a..7f47d57ac3 100644 --- a/plugins/jenkins-backend/src/service/standaloneServer.ts +++ b/plugins/jenkins-backend/src/service/standaloneServer.ts @@ -18,7 +18,7 @@ import { createServiceBuilder } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; -import { EntityRef } from '@backstage/catalog-model'; +import { EntityName } from '@backstage/catalog-model'; import { JenkinsInfo } from './jenkinsInfoProvider'; export interface ServerOptions { @@ -35,7 +35,7 @@ export async function startStandaloneServer( const router = await createRouter({ logger, jenkinsInfoProvider: { - async getInstance(_: { entityRef: EntityRef }): Promise { + async getInstance(_: { entityRef: EntityName }): Promise { return { baseUrl: 'https://example.com/', jobFullName: 'build-foo' }; }, }, diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 8611c9d2b7..33d1fa2508 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-jenkins +## 0.6.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- 67a7c02d26: Remove usages of `EntityRef` and `parseEntityName` from `@backstage/catalog-model` +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.5.21 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.5.20 ### Patch Changes diff --git a/plugins/jenkins/api-report.md b/plugins/jenkins/api-report.md index 61c5327fa1..5db5fd0dfe 100644 --- a/plugins/jenkins/api-report.md +++ b/plugins/jenkins/api-report.md @@ -10,7 +10,6 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import type { EntityName } from '@backstage/catalog-model'; -import type { EntityRef } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -18,9 +17,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; // Warning: (ae-missing-release-tag) "EntityJenkinsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityJenkinsContent: (_props: { - entity?: Entity | undefined; -}) => JSX.Element; +export const EntityJenkinsContent: (_props: {}) => JSX.Element; // Warning: (ae-missing-release-tag) "EntityLatestJenkinsRunCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -57,7 +54,7 @@ export interface JenkinsApi { }): Promise; // Warning: (ae-forgotten-export) The symbol "Project" needs to be exported by the entry point index.d.ts getProjects(options: { - entity: EntityRef; + entity: EntityName; filter: { branch?: string; }; @@ -84,31 +81,20 @@ export class JenkinsClient implements JenkinsApi { identityApi: IdentityApi; }); // (undocumented) - getBuild({ - entity, - jobFullName, - buildNumber, - }: { + getBuild(options: { entity: EntityName; jobFullName: string; buildNumber: string; }): Promise; // (undocumented) - getProjects({ - entity, - filter, - }: { + getProjects(options: { entity: EntityName; filter: { branch?: string; }; }): Promise; // (undocumented) - retry({ - entity, - jobFullName, - buildNumber, - }: { + retry(options: { entity: EntityName; jobFullName: string; buildNumber: string; diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 7b1283cd23..ada061dba9 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.5.20", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,22 +25,22 @@ "jenkins" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -50,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/jenkins/src/api/JenkinsApi.ts b/plugins/jenkins/src/api/JenkinsApi.ts index 65ac79b62b..dd169de78b 100644 --- a/plugins/jenkins/src/api/JenkinsApi.ts +++ b/plugins/jenkins/src/api/JenkinsApi.ts @@ -19,7 +19,7 @@ import { DiscoveryApi, IdentityApi, } from '@backstage/core-plugin-api'; -import type { EntityName, EntityRef } from '@backstage/catalog-model'; +import type { EntityName } from '@backstage/catalog-model'; import { ResponseError } from '@backstage/errors'; export const jenkinsApiRef = createApiRef({ @@ -80,7 +80,7 @@ export interface JenkinsApi { */ getProjects(options: { /** the entity whose jobs should be retrieved. */ - entity: EntityRef; + entity: EntityName; /** a filter on jobs. Currently this just takes a branch (and assumes certain structures in jenkins) */ filter: { branch?: string }; }): Promise; @@ -117,13 +117,11 @@ export class JenkinsClient implements JenkinsApi { this.identityApi = options.identityApi; } - async getProjects({ - entity, - filter, - }: { + async getProjects(options: { entity: EntityName; filter: { branch?: string }; }): Promise { + const { entity, filter } = options; const url = new URL( `${await this.discoveryApi.getBaseUrl( 'jenkins', @@ -158,15 +156,12 @@ export class JenkinsClient implements JenkinsApi { ); } - async getBuild({ - entity, - jobFullName, - buildNumber, - }: { + async getBuild(options: { entity: EntityName; jobFullName: string; buildNumber: string; }): Promise { + const { entity, jobFullName, buildNumber } = options; const url = `${await this.discoveryApi.getBaseUrl( 'jenkins', )}/v1/entity/${encodeURIComponent(entity.namespace)}/${encodeURIComponent( @@ -186,15 +181,12 @@ export class JenkinsClient implements JenkinsApi { return (await response.json()).build; } - async retry({ - entity, - jobFullName, - buildNumber, - }: { + async retry(options: { entity: EntityName; jobFullName: string; buildNumber: string; }): Promise { + const { entity, jobFullName, buildNumber } = options; const url = `${await this.discoveryApi.getBaseUrl( 'jenkins', )}/v1/entity/${encodeURIComponent(entity.namespace)}/${encodeURIComponent( diff --git a/plugins/jenkins/src/components/Router.tsx b/plugins/jenkins/src/components/Router.tsx index 0d38fe009a..b3561be2b0 100644 --- a/plugins/jenkins/src/components/Router.tsx +++ b/plugins/jenkins/src/components/Router.tsx @@ -27,10 +27,7 @@ export const isJenkinsAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[JENKINS_ANNOTATION]) || Boolean(entity.metadata.annotations?.[LEGACY_JENKINS_ANNOTATION]); -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; +type Props = {}; export const Router = (_props: Props) => { const { entity } = useEntity(); diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index cd80463672..766f6e9dfb 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-kafka-backend +## 0.2.20 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + +## 0.2.19 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + +## 0.2.18 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + ## 0.2.17 ### Patch Changes diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 01b23ba696..f10020b6d6 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.17", + "version": "0.2.20", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -23,19 +26,19 @@ ], "configSchema": "config.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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -44,7 +47,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/jest-when": "^2.7.2", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index e73c3a6efb..578e05fe33 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-kafka +## 0.3.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.29 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.2.28 ### Patch Changes diff --git a/plugins/kafka/api-report.md b/plugins/kafka/api-report.md index 0750978b27..95eed1dc13 100644 --- a/plugins/kafka/api-report.md +++ b/plugins/kafka/api-report.md @@ -12,9 +12,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; // Warning: (ae-missing-release-tag) "EntityKafkaContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityKafkaContent: (_props: { - entity?: Entity | undefined; -}) => JSX.Element; +export const EntityKafkaContent: (_props: {}) => JSX.Element; // Warning: (ae-missing-release-tag) "isPluginApplicableToEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 12dd578daa..ea979e37f9 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.2.28", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,22 +10,25 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,10 +39,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/kafka/src/Router.tsx b/plugins/kafka/src/Router.tsx index c63019253b..133326b5b9 100644 --- a/plugins/kafka/src/Router.tsx +++ b/plugins/kafka/src/Router.tsx @@ -25,10 +25,7 @@ import { MissingAnnotationEmptyState } from '@backstage/core-components'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION]); -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; +type Props = {}; export const Router = (_props: Props) => { const { entity } = useEntity(); diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 457e17e989..d6473619cb 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-kubernetes-backend +## 0.4.10 + +### Patch Changes + +- 64acf65c03: Allow missing kubernetes config in development env +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/plugin-kubernetes-common@0.2.5 + +## 0.4.9 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/plugin-kubernetes-common@0.2.4 + +## 0.4.8 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 0107c9aa08: chore(deps): bump `helmet` from 4.4.1 to 5.0.2 +- fb09a59a3f: Fixed a potential issue in AWS token encoding, where they might not always be properly converted to URL-safe base64. +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/plugin-kubernetes-common@0.2.3 + ## 0.4.7 ### Patch Changes diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 9b181c8514..ea273d7632 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -83,15 +83,7 @@ export interface GKEClusterDetails extends ClusterDetails {} export class KubernetesBuilder { constructor(env: KubernetesEnvironment); // (undocumented) - build(): Promise<{ - clusterDetails: ClusterDetails[]; - clusterSupplier: KubernetesClustersSupplier; - customResources: CustomResource[]; - fetcher: KubernetesFetcher; - objectsProvider: KubernetesObjectsProvider; - router: express.Router; - serviceLocator: KubernetesServiceLocator; - }>; + build(): KubernetesBuilderReturn; // (undocumented) protected buildClusterSupplier(): KubernetesClustersSupplier; // (undocumented) @@ -142,6 +134,17 @@ export class KubernetesBuilder { setServiceLocator(serviceLocator?: KubernetesServiceLocator): this; } +// @public +export type KubernetesBuilderReturn = Promise<{ + router: express.Router; + clusterDetails: ClusterDetails[]; + clusterSupplier: KubernetesClustersSupplier; + customResources: CustomResource[]; + fetcher: KubernetesFetcher; + objectsProvider: KubernetesObjectsProvider; + serviceLocator: KubernetesServiceLocator; +}>; + // Warning: (ae-missing-release-tag) "KubernetesClustersSupplier" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index feb6a4defd..fa387a9191 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.4.7", + "version": "0.4.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -23,20 +26,20 @@ ], "configSchema": "schema.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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-kubernetes-common": "^0.2.2", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-kubernetes-common": "^0.2.5", "@google-cloud/container": "^2.2.0", "@kubernetes/client-node": "^0.16.0", "@types/express": "^4.17.6", @@ -47,7 +50,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", - "helmet": "^4.0.0", + "helmet": "^5.0.2", "lodash": "^4.17.21", "morgan": "^1.10.0", "stream-buffers": "^3.0.2", @@ -55,7 +58,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/aws4": "^1.5.1", "supertest": "^6.1.3", "aws-sdk-mock": "^5.2.1", diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index 4af39d4f26..9f1353ca5c 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -18,19 +18,6 @@ import { sign } from 'aws4'; import { AWSClusterDetails } from '../types/types'; import { KubernetesAuthTranslator } from './types'; -const base64 = (str: string) => - Buffer.from(str.toString(), 'binary').toString('base64'); -const prepend = (prep: string) => (str: string) => prep + str; -const replace = - (search: string | RegExp, substitution: string) => (str: string) => - str.replace(search, substitution); -const pipe = - (fns: ReadonlyArray) => - (thing: string): string => - fns.reduce((val, fn) => fn(val), thing); -const removePadding = replace(/=+$/, ''); -const makeUrlSafe = pipe([replace('+', '-'), replace('/', '_')]); - type SigningCreds = { accessKeyId: string | undefined; secretAccessKey: string | undefined; @@ -117,15 +104,15 @@ export class AwsIamKubernetesAuthTranslator signQuery: true, }; - const signedRequest = sign(request, credentials); + const signed = sign(request, credentials); + const url = `https://${signed.host}${signed.path}`; + const base64Url = Buffer.from(url, 'binary').toString('base64'); + const urlSafeBase64Url = base64Url + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); - return pipe([ - (signed: any) => `https://${signed.host}${signed.path}`, - base64, - removePadding, - makeUrlSafe, - prepend('k8s-aws-v1.'), - ])(signedRequest); + return `k8s-aws-v1.${urlSafeBase64Url}`; } async decorateClusterDetailsWithAuth( diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 3333f7a0fc..a1eb60a239 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -43,6 +43,21 @@ export interface KubernetesEnvironment { config: Config; } +/** + * The return type of the `KubernetesBuilder.build` method + * + * @public + */ +export type KubernetesBuilderReturn = Promise<{ + router: express.Router; + clusterDetails: ClusterDetails[]; + clusterSupplier: KubernetesClustersSupplier; + customResources: CustomResource[]; + fetcher: KubernetesFetcher; + objectsProvider: KubernetesObjectsProvider; + serviceLocator: KubernetesServiceLocator; +}>; + export class KubernetesBuilder { private clusterSupplier?: KubernetesClustersSupplier; private objectsProvider?: KubernetesObjectsProvider; @@ -55,11 +70,23 @@ export class KubernetesBuilder { constructor(protected readonly env: KubernetesEnvironment) {} - public async build() { + public async build(): KubernetesBuilderReturn { const logger = this.env.logger; + const config = this.env.config; logger.info('Initializing Kubernetes backend'); + if (!config.has('kubernetes')) { + if (process.env.NODE_ENV !== 'development') { + throw new Error('Kubernetes configuration is missing'); + } + logger.warn( + 'Failed to initialize kubernetes backend: kubernetes config is missing', + ); + return { + router: Router(), + } as unknown as KubernetesBuilderReturn; + } const customResources = this.buildCustomResources(); const fetcher = this.fetcher ?? this.buildFetcher(); diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 785a5c619d..4016be8e1b 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-kubernetes-common +## 0.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.11.0 + +## 0.2.4 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/catalog-model@0.10.1 + +## 0.2.3 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/catalog-model@0.10.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index cd12eb3519..229cf01d40 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.2.2", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -24,22 +27,22 @@ "dist" ], "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "bugs": { "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", + "@backstage/catalog-model": "^0.11.0", "@kubernetes/client-node": "^0.16.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0" + "@backstage/cli": "^0.14.1" }, "jest": { "roots": [ diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 33abbb9255..c729333b2a 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-kubernetes +## 0.6.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/plugin-kubernetes-common@0.2.5 + +## 0.5.8 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + - @backstage/plugin-kubernetes-common@0.2.3 + ## 0.5.7 ### Patch Changes diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 8dc0114738..f635a61cef 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -25,9 +25,7 @@ export const clusterLinksFormatters: Record; // Warning: (ae-missing-release-tag) "EntityKubernetesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityKubernetesContent: (_props: { - entity?: Entity | undefined; -}) => JSX.Element; +export const EntityKubernetesContent: (_props: {}) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "FormatClusterLinkOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "formatClusterLink" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 1a60997405..6b3c90e396 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.5.7", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,24 +24,24 @@ "kubernetes" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/plugin-kubernetes-common": "^0.2.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-kubernetes-common": "^0.2.5", "@kubernetes/client-node": "^0.16.0", - "@backstage/theme": "^0.2.14", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -53,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/kubernetes/src/Router.tsx b/plugins/kubernetes/src/Router.tsx index 873bf128e4..deb6475ad5 100644 --- a/plugins/kubernetes/src/Router.tsx +++ b/plugins/kubernetes/src/Router.tsx @@ -32,10 +32,7 @@ export const isKubernetesAvailable = (entity: Entity) => entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION], ); -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; +type Props = {}; export const Router = (_props: Props) => { const { entity } = useEntity(); diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 541861b519..18dc47bb44 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-lighthouse +## 0.3.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.38 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + ## 0.2.37 ### Patch Changes diff --git a/plugins/lighthouse/api-report.md b/plugins/lighthouse/api-report.md index 724f8898a6..26db053ef0 100644 --- a/plugins/lighthouse/api-report.md +++ b/plugins/lighthouse/api-report.md @@ -70,9 +70,7 @@ export const EntityLastLighthouseAuditCard: ({ // Warning: (ae-missing-release-tag) "EntityLighthouseContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityLighthouseContent: (_props: { - entity?: Entity | undefined; -}) => JSX.Element; +export const EntityLighthouseContent: (_props: {}) => JSX.Element; // Warning: (ae-missing-release-tag) "FetchError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 618202b54c..219f6bf8d9 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.2.37", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,22 +25,22 @@ "lighthouse" ], "scripts": { - "build": "backstage-cli plugin:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "clean": "backstage-cli clean", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "start": "backstage-cli plugin:serve" + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +51,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/lighthouse/src/Router.tsx b/plugins/lighthouse/src/Router.tsx index 917001c591..1c41f522f4 100644 --- a/plugins/lighthouse/src/Router.tsx +++ b/plugins/lighthouse/src/Router.tsx @@ -36,10 +36,7 @@ export const Router = () => ( ); -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; +type Props = {}; export const EmbeddedRouter = (_props: Props) => { const { entity } = useEntity(); diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index e1101d2369..98f751fb92 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-newrelic-dashboard +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.7 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + ## 0.1.6 ### Patch Changes diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index dba9cc1148..d75a96a30e 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.6", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,30 +9,33 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", "@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.2", - "@backstage/dev-utils": "^0.2.21", + "@backstage/cli": "^0.14.1", + "@backstage/dev-utils": "^0.2.23", "@testing-library/jest-dom": "^5.10.1", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5" diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 821de4582a..f25d105a3c 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-newrelic +## 0.3.18 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + +## 0.3.17 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/theme@0.2.15 + ## 0.3.16 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 9f9d51eae3..1f7b1a8fae 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.16", + "version": "0.3.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,19 +25,19 @@ "newrelic" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,10 +47,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 76b3945a5d..c9cc96bbe6 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-org +## 0.5.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- cd8c6970ed: Add entity sync button to Group page +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.4.3 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 538ca90790: Use updated type names from `@backstage/catalog-client` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + +## 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 diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md index b181a3aab4..723cbd43b7 100644 --- a/plugins/org/api-report.md +++ b/plugins/org/api-report.md @@ -6,11 +6,8 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { Entity } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; -import { GroupEntity } from '@backstage/catalog-model'; import { InfoCardVariants } from '@backstage/core-components'; -import { UserEntity } from '@backstage/catalog-model'; // Warning: (ae-missing-release-tag) "EntityGroupProfileCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -18,7 +15,6 @@ import { UserEntity } from '@backstage/catalog-model'; export const EntityGroupProfileCard: ({ variant, }: { - entity?: GroupEntity | undefined; variant?: InfoCardVariants | undefined; }) => JSX.Element; @@ -26,7 +22,6 @@ export const EntityGroupProfileCard: ({ // // @public (undocumented) export const EntityMembersListCard: (_props: { - entity?: GroupEntity | undefined; memberDisplayTitle?: string | undefined; pageSize?: number | undefined; }) => JSX.Element; @@ -38,7 +33,6 @@ export const EntityOwnershipCard: ({ variant, entityFilterKind, }: { - entity?: Entity | undefined; variant?: InfoCardVariants | undefined; entityFilterKind?: string[] | undefined; }) => JSX.Element; @@ -49,7 +43,6 @@ export const EntityOwnershipCard: ({ export const EntityUserProfileCard: ({ variant, }: { - entity?: UserEntity | undefined; variant?: InfoCardVariants | undefined; }) => JSX.Element; @@ -59,7 +52,6 @@ export const EntityUserProfileCard: ({ export const GroupProfileCard: ({ variant, }: { - entity?: GroupEntity | undefined; variant?: InfoCardVariants | undefined; }) => JSX.Element; @@ -67,7 +59,6 @@ export const GroupProfileCard: ({ // // @public (undocumented) export const MembersListCard: (_props: { - entity?: GroupEntity; memberDisplayTitle?: string; pageSize?: number; }) => JSX.Element; @@ -91,7 +82,6 @@ export const OwnershipCard: ({ variant, entityFilterKind, }: { - entity?: Entity | undefined; variant?: InfoCardVariants | undefined; entityFilterKind?: string[] | undefined; }) => JSX.Element; @@ -102,7 +92,6 @@ export const OwnershipCard: ({ export const UserProfileCard: ({ variant, }: { - entity?: UserEntity | undefined; variant?: InfoCardVariants | undefined; }) => JSX.Element; ``` diff --git a/plugins/org/package.json b/plugins/org/package.json index 58f293fa8c..93c8a2660a 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.2-next.0", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,22 +10,25 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,11 +42,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@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", + "@backstage/catalog-client": "^0.7.2", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx index dc90a46fdf..fc45d9a8fe 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx @@ -15,19 +15,20 @@ */ import { GroupEntity } from '@backstage/catalog-model'; -import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; -import React from 'react'; -import { MemoryRouter } from 'react-router'; +import React, { ComponentType } from 'react'; import { GroupProfileCard } from './GroupProfileCard'; -export default { - title: 'Plugins/Org/Group Profile Card', - component: GroupProfileCard, -}; - const dummyDepartment = { type: 'childOf', + targetRef: 'group:default/department-a', target: { namespace: 'default', kind: 'group', @@ -55,14 +56,34 @@ const defaultEntity: GroupEntity = { relations: [dummyDepartment], }; +const catalogApi: Partial = { + async refreshEntity() {}, +}; + export const Default = () => ( - - - - - - + + + + - - + +
); + +export default { + title: 'Plugins/Org/Group Profile Card', + component: GroupProfileCard, + decorators: [ + (Story: ComponentType<{}>) => + wrapInTestApp( + + + , + { + mountedRoutes: { + '/a': entityRouteRef, + }, + }, + ), + ], +}; diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index b44fbe6c5c..88167eb062 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -18,12 +18,15 @@ import { GroupEntity, RELATION_CHILD_OF, RELATION_PARENT_OF, + ANNOTATION_LOCATION, + stringifyEntityRef, + ANNOTATION_EDIT_URL, } from '@backstage/catalog-model'; import { + catalogApiRef, EntityRefLinks, getEntityRelations, useEntity, - getEntityMetadataEditUrl, } from '@backstage/plugin-catalog-react'; import { Box, @@ -39,14 +42,16 @@ import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; import EditIcon from '@material-ui/icons/Edit'; +import CachedIcon from '@material-ui/icons/Cached'; import Alert from '@material-ui/lab/Alert'; -import React from 'react'; +import React, { useCallback } from 'react'; import { Avatar, InfoCard, InfoCardVariants, Link, } from '@backstage/core-components'; +import { alertApiRef, useApi } from '@backstage/core-plugin-api'; const CardTitle = ({ title }: { title: string }) => ( @@ -58,17 +63,23 @@ const CardTitle = ({ title }: { title: string }) => ( export const GroupProfileCard = ({ variant, }: { - /** @deprecated The entity is now grabbed from context instead */ - entity?: GroupEntity; variant?: InfoCardVariants; }) => { + const catalogApi = useApi(catalogApiRef); + const alertApi = useApi(alertApiRef); const { entity: group } = useEntity(); + + const refreshEntity = useCallback(async () => { + await catalogApi.refreshEntity(stringifyEntityRef(group)); + alertApi.post({ message: 'Refresh scheduled', severity: 'info' }); + }, [catalogApi, alertApi, group]); + if (!group) { return Group not found; } const { - metadata: { name, description }, + metadata: { name, description, annotations }, spec: { profile }, } = group; @@ -79,7 +90,12 @@ export const GroupProfileCard = ({ kind: 'group', }); - const entityMetadataEditUrl = getEntityMetadataEditUrl(group); + const entityLocation = annotations?.[ANNOTATION_LOCATION]; + const allowRefresh = + entityLocation?.startsWith('url:') || entityLocation?.startsWith('file:'); + + const entityMetadataEditUrl = + group.metadata.annotations?.[ANNOTATION_EDIT_URL]; const displayName = profile?.displayName ?? name; const emailHref = profile?.email ? `mailto:${profile.email}` : '#'; @@ -103,7 +119,20 @@ export const GroupProfileCard = ({ title={} subheader={description} variant={variant} - action={infoCardAction} + action={ + <> + {allowRefresh && ( + + + + )} + {infoCardAction} + + } > diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx index b6b549d934..b0a7ed8c70 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx @@ -54,6 +54,7 @@ const makeUser = ({ relations: [ { type: 'memberOf', + targetRef: 'group:default/team-a', target: { namespace: 'default', kind: 'group', diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 99be8571e0..cffe3e0228 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -59,6 +59,7 @@ describe('MemberTab Test', () => { relations: [ { type: 'memberOf', + targetRef: 'group:default/team-d', target: { kind: 'group', name: 'team-d', diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index 9a107f2d8f..67bf5c2e42 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, GroupEntity, UserEntity, stringifyEntityRef, @@ -109,8 +109,6 @@ const MemberComponent = ({ member }: { member: UserEntity }) => { }; export const MembersListCard = (_props: { - /** @deprecated The entity is now grabbed from context instead */ - entity?: GroupEntity; memberDisplayTitle?: string; pageSize?: number; }) => { @@ -124,7 +122,7 @@ export const MembersListCard = (_props: { const displayName = profile?.displayName ?? groupName; - const groupNamespace = grpNamespace || ENTITY_DEFAULT_NAMESPACE; + const groupNamespace = grpNamespace || DEFAULT_NAMESPACE; const [page, setPage] = React.useState(1); const pageChange = (_: React.ChangeEvent, pageIndex: number) => { diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx index a0536e1a74..c07815f39e 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx @@ -69,9 +69,10 @@ const makeComponent = ({ type, name }: { type: string; name: string }) => ({ relations: [ { type: 'ownedBy', + targetRef: 'group:default/team-a', target: { namespace: 'default', - kind: 'Group', + kind: 'group', name: 'team-a', }, }, diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index 26a79dfaee..3fc770e6b5 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 { @@ -43,10 +43,11 @@ const items = [ relations: [ { type: 'ownedBy', + targetRef: 'group:default/my-team', target: { name: 'my-team', namespace: 'default', - kind: 'Group', + kind: 'group', }, }, ], @@ -62,10 +63,11 @@ const items = [ relations: [ { type: 'ownedBy', + targetRef: 'group:default/my-team', target: { name: 'my-team', namespace: 'default', - kind: 'Group', + kind: 'group', }, }, ], @@ -82,10 +84,11 @@ const items = [ relations: [ { type: 'ownedBy', + targetRef: 'group:default/my-team', target: { name: 'my-team', namespace: 'default', - kind: 'Group', + kind: 'group', }, }, ], @@ -99,10 +102,11 @@ const items = [ relations: [ { type: 'ownedBy', + targetRef: 'group:default/my-team', target: { name: 'my-team', namespace: 'default', - kind: 'Group', + kind: 'group', }, }, ], @@ -110,15 +114,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', () => { @@ -135,9 +139,10 @@ describe('OwnershipCard', () => { relations: [ { type: 'memberOf', + targetRef: 'group:default/examplegroup', target: { kind: 'group', - name: 'ExampleGroup', + name: 'examplegroup', namespace: 'default', }, }, @@ -260,8 +265,9 @@ describe('OwnershipCard', () => { relations: [ { type: 'memberOf', + targetRef: 'group:default/my-team', target: { - kind: 'Group', + kind: 'group', name: 'my-team', namespace: 'default', }, diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index c89534ea7a..cbf517bdca 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -25,7 +25,7 @@ import { import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { catalogApiRef, - formatEntityRefTitle, + humanizeEntityRef, isOwnerOf, useEntity, } from '@backstage/plugin-catalog-react'; @@ -108,7 +108,7 @@ const getQueryParams = ( owner: Entity, selectedEntity: EntityTypeProps, ): string => { - const ownerName = formatEntityRefTitle(owner, { defaultKind: 'group' }); + const ownerName = humanizeEntityRef(owner, { defaultKind: 'group' }); const { kind, type } = selectedEntity; const filters = { kind, @@ -131,8 +131,6 @@ export const OwnershipCard = ({ variant, entityFilterKind, }: { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; variant?: InfoCardVariants; entityFilterKind?: string[]; }) => { 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 0cb3987c77..eb7b8fadda 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx @@ -15,19 +15,18 @@ */ import { UserEntity } from '@backstage/catalog-model'; -import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { + EntityProvider, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { wrapInTestApp } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; -import React from 'react'; -import { MemoryRouter } from 'react-router'; +import React, { ComponentType } from 'react'; import { UserProfileCard } from './UserProfileCard'; -export default { - title: 'Plugins/Org/User Profile Card', - component: UserProfileCard, -}; - const dummyGroup = { type: 'memberOf', + targetRef: 'group:default/team-a', target: { namespace: 'default', kind: 'group', @@ -55,15 +54,13 @@ const defaultEntity: UserEntity = { }; export const Default = () => ( - - - - - - + + + + - - + + ); const noImageEntity: UserEntity = { @@ -84,13 +81,24 @@ const noImageEntity: UserEntity = { }; export const NoImage = () => ( - - - - - - + + + + - - + + ); + +export default { + title: 'Plugins/Org/User Profile Card', + component: UserProfileCard, + decorators: [ + (Story: ComponentType<{}>) => + wrapInTestApp(, { + mountedRoutes: { + '/a': entityRouteRef, + }, + }), + ], +}; 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 7b9904954f..3e5e595eef 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx @@ -42,6 +42,7 @@ describe('UserSummary Test', () => { relations: [ { type: 'memberOf', + targetRef: 'group:default/examplegroup', target: { kind: 'group', name: 'ExampleGroup', @@ -55,7 +56,7 @@ describe('UserSummary Test', () => { const rendered = await renderWithEffects( wrapInTestApp( - + , { mountedRoutes: { @@ -70,9 +71,9 @@ describe('UserSummary Test', () => { 'src', 'https://example.com/staff/calum.jpeg', ); - expect(rendered.getByText('ExampleGroup')).toHaveAttribute( + expect(rendered.getByText('examplegroup')).toHaveAttribute( 'href', - '/catalog/default/group/ExampleGroup', + '/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 0997e0a4d8..bafaad7ba0 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -51,8 +51,6 @@ const CardTitle = ({ title }: { title?: string }) => export const UserProfileCard = ({ variant, }: { - /** @deprecated The entity is now grabbed from context instead */ - entity?: UserEntity; variant?: InfoCardVariants; }) => { const { entity: user } = useEntity(); diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index 6fbcfc54b1..c5a4e87923 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-pagerduty +## 0.3.27 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.3.26 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- edeeac7e9d: Switch `luxon` dependency range to be `^2.0.2` rather than `2.0.2`. +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.3.25 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 7912a32f15..8a7fcee37e 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.25", + "version": "0.3.27", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,26 +24,26 @@ "pagerduty" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "classnames": "^2.2.6", - "luxon": "2.0.2", + "luxon": "^2.0.2", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, @@ -49,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx index ef0659becb..171238cdd3 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx @@ -34,10 +34,7 @@ import { alertApiRef, identityApiRef, } from '@backstage/core-plugin-api'; -import { - ENTITY_DEFAULT_NAMESPACE, - parseEntityRef, -} from '@backstage/catalog-model'; +import { DEFAULT_NAMESPACE, parseEntityRef } from '@backstage/catalog-model'; type Props = { showDialog: boolean; @@ -61,7 +58,7 @@ export const TriggerDialog = ({ const { userEntityRef } = await identityApi.getBackstageIdentity(); const { name: userName } = parseEntityRef(userEntityRef, { defaultKind: 'User', - defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + defaultNamespace: DEFAULT_NAMESPACE, }); await api.triggerAlarm({ integrationKey: integrationKey as string, diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 58ace0ff17..7ec949ff06 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-permission-backend +## 0.5.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-permission-node@0.5.2 + - @backstage/plugin-auth-node@0.1.3 + +## 0.5.1 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/plugin-auth-node@0.1.2 + - @backstage/plugin-permission-common@0.5.1 + - @backstage/plugin-permission-node@0.5.1 + +## 0.5.0 + +### Minor Changes + +- e2cf0662eb: 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, + }); + } + ``` + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/plugin-auth-node@0.1.1 + - @backstage/plugin-permission-common@0.5.0 + - @backstage/config@0.1.14 + - @backstage/plugin-permission-node@0.5.0 + ## 0.4.3 ### Patch Changes diff --git a/plugins/permission-backend/api-report.md b/plugins/permission-backend/api-report.md index 843c9a315c..2b59113535 100644 --- a/plugins/permission-backend/api-report.md +++ b/plugins/permission-backend/api-report.md @@ -3,6 +3,7 @@ > 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-node'; import { Logger as Logger_2 } from 'winston'; @@ -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 e1a4e10c5c..30f8f45dd8 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.3", + "version": "0.5.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,22 +9,25 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-auth-node": "^0.1.0", - "@backstage/plugin-permission-common": "^0.4.0", - "@backstage/plugin-permission-node": "^0.4.3", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-auth-node": "^0.1.3", + "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-permission-node": "^0.5.2", "@types/express": "*", "dataloader": "^2.0.0", "express": "^4.17.1", @@ -36,7 +39,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@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 498fcb748b..b98b32b01c 100644 --- a/plugins/permission-backend/src/service/router.test.ts +++ b/plugins/permission-backend/src/service/router.test.ts @@ -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 413f3e6b45..48f7b1a2e5 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -44,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(), @@ -79,6 +80,7 @@ export interface RouterOptions { discovery: PluginEndpointDiscovery; policy: PermissionPolicy; identity: IdentityClient; + config: Config; } const handleRequest = async ( @@ -139,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, diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index c0c21288be..b7162ecb06 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-permission-common +## 0.5.1 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + +## 0.5.0 + +### Minor Changes + +- 8c646beb24: **BREAKING** `PermissionCriteria` now requires at least one condition in `anyOf` and `allOf` arrays. This addresses some ambiguous behavior outlined in #9280. + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/errors@0.2.1 + - @backstage/config@0.1.14 + ## 0.4.0 ### Minor Changes 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 d3054fe9fe..122106da8f 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-common", "description": "Isomorphic types and client for Backstage permissions and authorization", - "version": "0.4.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -10,6 +10,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -27,25 +30,25 @@ ], "configSchema": "config.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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "bugs": { "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", "cross-fetch": "^3.1.5", "uuid": "^8.0.0", "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", + "@backstage/cli": "^0.14.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-common/src/types/permission.ts b/plugins/permission-common/src/types/permission.ts index 805aefa3de..16934d8771 100644 --- a/plugins/permission-common/src/types/permission.ts +++ b/plugins/permission-common/src/types/permission.ts @@ -37,8 +37,22 @@ export type PermissionAttributes = { * @public */ export type Permission = { + /** + * The name of the permission. + */ name: string; + /** + * {@link PermissionAttributes} which describe characteristics of the permission, to help + * policy authors make consistent decisions for similar permissions without referring to them + * all by name. + */ attributes: PermissionAttributes; + /** + * Some permissions can be authorized based on characteristics of a resource + * such a catalog entity. For these permissions, the resourceType field + * denotes the type of the resource whose resourceRef should be passed when + * authorizing. + */ resourceType?: string; }; diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index d79c79159f..74ea9b927b 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-permission-node +## 0.5.2 + +### Patch Changes + +- 0816f8237a: Improved error message shown when permissions are enabled without backend-to-backend authentication. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-auth-node@0.1.3 + +## 0.5.1 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/plugin-auth-node@0.1.2 + - @backstage/plugin-permission-common@0.5.1 + +## 0.5.0 + +### Minor Changes + +- 8c646beb24: **BREAKING** `PermissionCriteria` now requires at least one condition in `anyOf` and `allOf` arrays. This addresses some ambiguous behavior outlined in #9280. + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/plugin-auth-node@0.1.1 + - @backstage/plugin-permission-common@0.5.0 + - @backstage/config@0.1.14 + ## 0.4.3 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 04a567f16b..7fb5542c27 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.3", + "version": "0.5.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "node-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,26 +24,27 @@ "permissions" ], "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config": "^0.1.13", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-auth-node": "^0.1.0", - "@backstage/plugin-permission-common": "^0.4.0", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-auth-node": "^0.1.3", + "@backstage/plugin-permission-common": "^0.5.1", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/permission-node/src/ServerPermissionClient.test.ts b/plugins/permission-node/src/ServerPermissionClient.test.ts index 9fa1eadb5e..53300061a0 100644 --- a/plugins/permission-node/src/ServerPermissionClient.test.ts +++ b/plugins/permission-node/src/ServerPermissionClient.test.ts @@ -113,7 +113,7 @@ describe('ServerPermissionClient', () => { tokenManager: ServerTokenManager.noop(), }), ).toThrowError( - 'You must configure at least one key in backend.auth.keys if permissions are enabled.', + 'Backend-to-backend authentication must be configured before enabling permissions. Read more here https://backstage.io/docs/tutorials/backend-to-backend-auth', ); }); }); diff --git a/plugins/permission-node/src/ServerPermissionClient.ts b/plugins/permission-node/src/ServerPermissionClient.ts index f22779c8e8..8c03016268 100644 --- a/plugins/permission-node/src/ServerPermissionClient.ts +++ b/plugins/permission-node/src/ServerPermissionClient.ts @@ -56,7 +56,7 @@ export class ServerPermissionClient implements PermissionAuthorizer { (tokenManager as any).isInsecureServerTokenManager ) { throw new Error( - 'You must configure at least one key in backend.auth.keys if permissions are enabled.', + 'Backend-to-backend authentication must be configured before enabling permissions. Read more here https://backstage.io/docs/tutorials/backend-to-backend-auth', ); } 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-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 41c55b5a45..f83856d958 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-permission-react +## 0.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.7.0 + +## 0.3.1 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-permission-common@0.5.0 + - @backstage/config@0.1.14 + ## 0.3.0 ### Minor Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index ce6e2dbf54..9b338df822 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.3.0", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,6 +9,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "web-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -19,17 +22,18 @@ "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-permission-common": "^0.4.0", + "@backstage/config": "^0.1.15", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-permission-common": "^0.5.1", "cross-fetch": "^3.1.5", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", @@ -40,8 +44,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0", - "@backstage/test-utils": "^0.2.4-next.0", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7" diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 7460eddd5a..84699fb11f 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-proxy-backend +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + +## 0.2.20 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + +## 0.2.19 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/config@0.1.14 + ## 0.2.18 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 5336bbb098..8bdb7b72bd 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.18", + "version": "0.2.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -20,17 +23,17 @@ "backstage" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config": "^0.1.13", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -43,7 +46,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 14e566219a..0292160304 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-rollbar-backend +## 0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + +## 0.1.23 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + +## 0.1.22 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 1433045c08: Removed unused `helmet` dependency. +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/config@0.1.14 + ## 0.1.21 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 36683ef66a..3f477e46d1 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.21", + "version": "0.1.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,17 +25,17 @@ "rollbar" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config": "^0.1.13", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "camelcase-keys": "^7.0.1", "compression": "^1.7.4", @@ -40,7 +43,6 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", - "helmet": "^4.0.0", "lodash": "^4.17.21", "morgan": "^1.10.0", "node-fetch": "^2.6.7", @@ -48,8 +50,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@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 df1cb19f83..d3dc72fc57 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-rollbar +## 0.4.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.3.27 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.3.26 ### Patch Changes diff --git a/plugins/rollbar/api-report.md b/plugins/rollbar/api-report.md index fbdbd36208..f666ade34c 100644 --- a/plugins/rollbar/api-report.md +++ b/plugins/rollbar/api-report.md @@ -12,18 +12,15 @@ import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EntityPageRollbar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityPageRollbar: (_props: Props) => JSX.Element; +export const EntityPageRollbar: () => JSX.Element; // Warning: (ae-missing-release-tag) "EntityRollbarContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityRollbarContent: (_props: { - entity?: Entity | undefined; -}) => JSX.Element; +export const EntityRollbarContent: (_props: {}) => JSX.Element; // Warning: (ae-missing-release-tag) "isPluginApplicableToEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -103,5 +100,5 @@ export { rollbarPlugin }; // Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: (_props: Props_2) => JSX.Element; +export const Router: (_props: Props) => JSX.Element; ``` diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 5767f26c5e..acd07071e5 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.26", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,21 +25,21 @@ "rollbar" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -50,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/rollbar/src/components/EntityPageRollbar/EntityPageRollbar.tsx b/plugins/rollbar/src/components/EntityPageRollbar/EntityPageRollbar.tsx index 8b51691778..626b3442e5 100644 --- a/plugins/rollbar/src/components/EntityPageRollbar/EntityPageRollbar.tsx +++ b/plugins/rollbar/src/components/EntityPageRollbar/EntityPageRollbar.tsx @@ -14,17 +14,11 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; import React from 'react'; import { RollbarProject } from '../RollbarProject/RollbarProject'; -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; - -export const EntityPageRollbar = (_props: Props) => { +export const EntityPageRollbar = () => { const { entity } = useEntity(); return ; diff --git a/plugins/rollbar/src/components/Router.tsx b/plugins/rollbar/src/components/Router.tsx index 032bf0166e..a11acb379c 100644 --- a/plugins/rollbar/src/components/Router.tsx +++ b/plugins/rollbar/src/components/Router.tsx @@ -25,10 +25,7 @@ import { MissingAnnotationEmptyState } from '@backstage/core-components'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[ROLLBAR_ANNOTATION]); -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; +type Props = {}; export const Router = (_props: Props) => { const { entity } = useEntity(); @@ -39,7 +36,7 @@ export const Router = (_props: Props) => { return ( - } /> + } /> ); }; diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 29ff4e9edb..00e5f08ba4 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.2 + +### Patch Changes + +- 0f37cdef19: Migrated over from the deprecated `spec.metadata` to `spec.templateInfo` for the `name` and the `baseUrl` of the template. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-scaffolder-backend@0.17.0 + - @backstage/integration@0.7.5 + +## 0.2.1 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + - @backstage/types@0.1.3 + - @backstage/plugin-scaffolder-backend@0.16.1 + +## 0.2.0 + +### Minor Changes + +- 661594bf43: Updated to the latest version of `@backstage/plugin-scaffolder-backend`, meaning the `TemplateAction` now exposes the precise input type rather than `any`. + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/plugin-scaffolder-backend@0.16.0 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.1.11 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md index 132d3d4225..e55c6368c4 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/api-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -6,6 +6,7 @@ /// import { ContainerRunner } from '@backstage/backend-common'; +import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; import { UrlReader } from '@backstage/backend-common'; @@ -15,5 +16,12 @@ export function createFetchCookiecutterAction(options: { reader: UrlReader; integrations: ScmIntegrations; containerRunner: ContainerRunner; -}): TemplateAction; +}): TemplateAction<{ + url: string; + targetPath?: string | undefined; + values: JsonObject; + copyWithoutRender?: string[] | undefined; + extensions?: string[] | undefined; + imageName?: string | undefined; +}>; ``` diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index cee138b576..2e878fc882 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.11", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,29 +10,32 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin-module" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/errors": "^0.2.0", - "@backstage/integration": "^0.7.2", - "@backstage/plugin-scaffolder-backend": "^0.15.24", - "@backstage/config": "^0.1.13", - "@backstage/types": "^0.1.1", + "@backstage/backend-common": "^0.11.0", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/config": "^0.1.15", + "@backstage/types": "^0.1.3", "command-exists": "^1.2.9", "fs-extra": "10.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index bba51e3f01..18a6722663 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -const runCommand = jest.fn(); +const executeShellCommand = jest.fn(); const commandExists = jest.fn(); const fetchContents = jest.fn(); jest.mock('@backstage/plugin-scaffolder-backend', () => ({ ...jest.requireActual('@backstage/plugin-scaffolder-backend'), fetchContents, - runCommand, + executeShellCommand, })); jest.mock('command-exists', () => commandExists); @@ -89,7 +89,10 @@ describe('fetch:cookiecutter', () => { help: 'me', }, }, - baseUrl: 'somebase', + templateInfo: { + entityRef: 'template:default/cookiecutter', + baseUrl: 'somebase', + }, workspacePath: mockTmpDir, logger: getVoidLogger(), logStream: new PassThrough(), @@ -112,8 +115,8 @@ describe('fetch:cookiecutter', () => { }); }); - // Mock when runCommand is called it creats some new files in the mock filesystem - runCommand.mockImplementation(async () => { + // Mock when executeShellCommand is called it creats some new files in the mock filesystem + executeShellCommand.mockImplementation(async () => { mockFs({ [`${join(mockTmpDir, 'intermediate')}`]: { 'testfile.json': '{}', @@ -149,7 +152,7 @@ describe('fetch:cookiecutter', () => { expect.objectContaining({ reader: mockReader, integrations, - baseUrl: mockContext.baseUrl, + baseUrl: mockContext.templateInfo?.baseUrl, fetchUrl: mockContext.input.url, outputPath: join( mockTmpDir, @@ -160,12 +163,12 @@ describe('fetch:cookiecutter', () => { ); }); - it('should call out to cookiecutter using runCommand when cookiecutter is installed', async () => { + it('should call out to cookiecutter using executeShellCommand when cookiecutter is installed', async () => { commandExists.mockResolvedValue(true); await action.handler(mockContext); - expect(runCommand).toHaveBeenCalledWith( + expect(executeShellCommand).toHaveBeenCalledWith( expect.objectContaining({ command: 'cookiecutter', args: [ diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index 01174e0f7a..4e09c81311 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -27,9 +27,9 @@ import fs from 'fs-extra'; import path, { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; import { - runCommand, createTemplateAction, fetchContents, + executeShellCommand, } from '@backstage/plugin-scaffolder-backend'; export class CookiecutterRunner { @@ -95,7 +95,7 @@ export class CookiecutterRunner { () => false, ); if (cookieCutterInstalled) { - await runCommand({ + await executeShellCommand({ command: 'cookiecutter', args: ['--no-input', '-o', intermediateDir, templateDir, '--verbose'], logStream, @@ -228,7 +228,7 @@ export function createFetchCookiecutterAction(options: { await fetchContents({ reader, integrations, - baseUrl: ctx.baseUrl, + baseUrl: ctx.templateInfo?.baseUrl, fetchUrl: ctx.input.url, outputPath: templateContentsDir, }); diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 2b369835f8..5a71d8d8ec 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.3.2 + +### Patch Changes + +- 0f37cdef19: Migrated over from the deprecated `spec.metadata` to `spec.templateInfo` for the `name` and the `baseUrl` of the template. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-scaffolder-backend@0.17.0 + - @backstage/integration@0.7.5 + +## 0.3.1 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + - @backstage/types@0.1.3 + - @backstage/plugin-scaffolder-backend@0.16.1 + +## 0.3.0 + +### Minor Changes + +- 661594bf43: Updated to the latest version of `@backstage/plugin-scaffolder-backend`, meaning the `TemplateAction` now exposes the precise input type rather than `any`. + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/plugin-scaffolder-backend@0.16.0 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.2.6 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/api-report.md b/plugins/scaffolder-backend-module-rails/api-report.md index 9c0610d4f2..aa631c5333 100644 --- a/plugins/scaffolder-backend-module-rails/api-report.md +++ b/plugins/scaffolder-backend-module-rails/api-report.md @@ -4,6 +4,7 @@ ```ts import { ContainerRunner } from '@backstage/backend-common'; +import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; import { UrlReader } from '@backstage/backend-common'; @@ -13,5 +14,10 @@ export function createFetchRailsAction(options: { reader: UrlReader; integrations: ScmIntegrations; containerRunner: ContainerRunner; -}): TemplateAction; +}): TemplateAction<{ + url: string; + targetPath?: string | undefined; + values: JsonObject; + imageName?: string | undefined; +}>; ``` diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 5f32dbadd8..f3dc8cd8e5 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.6", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,27 +11,30 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin-module" + }, "scripts": { - "build": "backstage-cli backend:build", - "start": "backstage-cli backend:dev", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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", - "@backstage/types": "^0.1.1", + "@backstage/backend-common": "^0.11.0", + "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/types": "^0.1.3", "command-exists": "^1.2.9", "fs-extra": "^9.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 0c7552f72f..0a6a996aa8 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -62,7 +62,10 @@ describe('fetch:rails', () => { help: 'me', }, }, - baseUrl: 'somebase', + templateInfo: { + baseUrl: 'somebase', + entityRef: 'template:default/myTemplate', + }, workspacePath: mockTmpDir, logger: getVoidLogger(), logStream: new PassThrough(), @@ -100,7 +103,7 @@ describe('fetch:rails', () => { expect(fetchContents).toHaveBeenCalledWith({ reader: mockReader, integrations, - baseUrl: mockContext.baseUrl, + baseUrl: mockContext.templateInfo.baseUrl, fetchUrl: mockContext.input.url, outputPath: resolvePath(mockContext.workspacePath), }); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index 2865361a8b..132929ee1c 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -170,7 +170,7 @@ export function createFetchRailsAction(options: { await fetchContents({ reader, integrations, - baseUrl: ctx.baseUrl, + baseUrl: ctx.templateInfo?.baseUrl, fetchUrl: ctx.input.url, outputPath: workDir, }); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts index 03d894f832..0767e63f29 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -const runCommand = jest.fn(); +const executeShellCommand = jest.fn(); const commandExists = jest.fn(); -jest.mock('@backstage/plugin-scaffolder-backend', () => ({ runCommand })); +jest.mock('@backstage/plugin-scaffolder-backend', () => ({ + executeShellCommand, +})); jest.mock('command-exists', () => commandExists); jest.mock('fs-extra'); @@ -195,7 +197,7 @@ describe('Rails Templater', () => { logStream: stream, }); - expect(runCommand).toHaveBeenCalledWith({ + expect(executeShellCommand).toHaveBeenCalledWith({ command: 'rails', args: expect.arrayContaining([ 'new', @@ -225,7 +227,7 @@ describe('Rails Templater', () => { logStream: stream, }); - expect(runCommand).toHaveBeenCalledWith({ + expect(executeShellCommand).toHaveBeenCalledWith({ command: 'rails', args: expect.arrayContaining([ 'new', diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts index f7817c7fb5..4a4672795a 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts @@ -17,7 +17,7 @@ import { ContainerRunner } from '@backstage/backend-common'; import fs from 'fs-extra'; import path from 'path'; -import { runCommand } from '@backstage/plugin-scaffolder-backend'; +import { executeShellCommand } from '@backstage/plugin-scaffolder-backend'; import commandExists from 'command-exists'; import { railsArgumentResolver, @@ -64,7 +64,7 @@ export class RailsNewRunner { railsArguments as RailsRunOptions, ); - await runCommand({ + await executeShellCommand({ command: baseCommand, args: [ ...baseArguments, diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 8c5a6ade44..f20f70b82e 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.17.0 + +## 0.2.0 + +### Minor Changes + +- 661594bf43: Updated to the latest version of `@backstage/plugin-scaffolder-backend`, meaning the `TemplateAction` now exposes the precise input type rather than `any`. + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- c82cd1b137: Bump `yeoman-environment` dependency from `^3.6.0` to `^3.9.1`. +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.16.0 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + ## 0.1.5 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/api-report.md b/plugins/scaffolder-backend-module-yeoman/api-report.md index fc18814faa..0f8747e592 100644 --- a/plugins/scaffolder-backend-module-yeoman/api-report.md +++ b/plugins/scaffolder-backend-module-yeoman/api-report.md @@ -3,8 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { JsonObject } from '@backstage/types'; import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; // @public -export function createRunYeomanAction(): TemplateAction; +export function createRunYeomanAction(): TemplateAction<{ + namespace: string; + args?: string[] | undefined; + options?: JsonObject | undefined; +}>; ``` diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index b2ce1bf571..609e8abab7 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.5", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,24 +10,27 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin-module" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/plugin-scaffolder-backend": "^0.15.24", - "@backstage/types": "^0.1.1", + "@backstage/config": "^0.1.15", + "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/types": "^0.1.3", "winston": "^3.2.1", "yeoman-environment": "^3.9.1" }, "devDependencies": { - "@backstage/backend-common": "^0.10.7", + "@backstage/backend-common": "^0.11.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 7c80464d71..25beb6c644 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,123 @@ # @backstage/plugin-scaffolder-backend +## 0.17.2 + +### Patch Changes + +- bug: `repoUrl` does not have a protocol in `publish:github:pull-request` + +## 0.17.1 + +### Patch Changes + +- bug: fixing `repoUrl` resolution for `publish:github:pull-request` action + +## 0.17.0 + +### Minor Changes + +- 91c6faeb7b: - **BREAKING** - the `/v2/tasks` endpoint now takes `templateRef` instead of `templateName` in the POST body. This should be a valid stringified `entityRef`. +- 7f193ff019: - **BREAKING** - `DatabaseTaskStore()` constructor is now removed. Please use the `DatabaseTaskStore.create()` method instead. + + - **BREAKING** - `TaskStore.createTask()` method now only takes one argument of type `TaskStoreCreateTaskOptions` which encapsulates the `spec` and `secrets` + + ```diff + - TaskStore.createTask(spec, secrets) + + TaskStore.createTask({ spec, secrets}) + ``` + + - **BREAKING** - `TaskBroker.dispatch()` method now only takes one argument of type `TaskBrokerDispatchOptions` which encapsulates the `spec` and `secrets` + + ```diff + - TaskBroker.dispatch(spec, secrets) + + TaskBroker.dispatch({ spec, secrets}) + ``` + +- 9d9b2bab47: - **BREAKING** - Removed the re-export of types `TaskSpec` `TaskSpecV1Beta2` and `TaskSpecV1Beta3` these should now be import from `@backstage/plugin-scaffolder-common` directly. + - **BREAKING** - Removed the `observe` method from the `TaskBroker` interface, this has now been replaced with an `Observable` implementation under `event# @backstage/plugin-scaffolder-backend. + +### Patch Changes + +- 9d9b2bab47: - **DEPRECATED** - Deprecated the `runCommand` export in favour of `executeShellCommand`. Please migrate to using the new method. + - Added a type parameter to `TaskStoreEmitOptions` to type the `body` property +- 65a7939c6c: - **DEPRECATED** - `TaskState` has been deprecated in favour of `CurrentClaimedTask` + - Narrowed the types from `JSONValue` to `JSONObject` as the usage is and should always be `JSONObject` for `complete` and `emitLog` `metadata` in `TaskContext` +- 67a7c02d26: Remove usages of `EntityRef` and `parseEntityName` from `@backstage/catalog-model` +- ed09ad8093: Updated usage of the `LocationSpec` type from `@backstage/catalog-model`, which is deprecated. +- 6981ac4ad2: - **DEPRECATED** - The `containerRunner` option passed to `createBuiltinActions` has now been deprecated. + + - **DEPRECATED** - The `createFetchCookiecutterAction` export has also been deprecated and will soon disappear from this plugin. + + The `fetch:cookiecutter` action will soon be removed from the default list of actions that are provided out of the box from the scaffolder plugin. It will still be supported, and maintained by the community, so you can install the package (`@backstage/plugin-scaffolder-backend-module-cookiecutter`) and pass it in as a custom action. Or you can migrate your templates to use [`fetch:template`](https://backstage.io/docs/features/software-templates/builtin-actions#migrating-from-fetchcookiecutter-to-fetchtemplate) with the `cookiecutterCompat` option. + +- b1744f1153: - **DEPRECATED** - `OctokitProvider` has been deprecated and will be removed in upcoming versions + This helper doesn't make sense to be export from the `plugin-scaffolder-backend` and possibly will be moved into the `integrations` package at a later date. + All implementations have been moved over to a private implementation called `getOctokitOptions` which is then passed to the `Octokit` constructor. If you're using this API you should consider duplicating the logic that lives in `getOctokitOptions` and move away from the deprecated export. +- 0f37cdef19: Migrated over from the deprecated `spec.metadata` to `spec.templateInfo` for the `name` and the `baseUrl` of the template. +- 7f193ff019: - **DEPRECATED** - `Status` has been deprecated in favour of `TaskStatus` + - **DEPRECATED** - `CompletedTaskState` has been deprecated in favour of `TaskCompletionState` + - **DEPRECATED** - `DispatchResult` has been deprecated in favour of `TaskBrokerDispatchResult` +- df61ca71dd: Implemented required `getProcessorName` method for catalog processor. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-catalog-backend@0.22.0 + - @backstage/plugin-scaffolder-common@0.2.2 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.2 + - @backstage/integration@0.7.5 + +## 0.16.1 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-client@0.7.1 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + - @backstage/types@0.1.3 + - @backstage/plugin-catalog-backend@0.21.5 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.1 + - @backstage/plugin-scaffolder-common@0.2.1 + +## 0.16.0 + +### Minor Changes + +- 661594bf43: **BREAKING**: Updated `TemplateAction` and related types to have its type parameter extend `JsonObject` instead of `InputBase`. The `createTemplateAction` has also been updated to pass through the `TInput` type parameter to the return type, meaning the `TemplateAction` retains its type. This can lead to breakages during type checking especially within tests. +- 8db2b671c6: **BREAKING**: `ctx.token` is now `ctx.secrets.backstageToken` in Actions. Please update any of your Actions that might call out to Backstage API's with this token. +- 5a1594330e: **BREAKING** - Removed the `CatalogEntityClient` export. This is no longer provider by this package, + but you can implement one pretty simply yourself using the `CatalogApi` and applying filters to fetch templates. +- 7d3471db94: Remove the previously deprecated `scaffolder.provider` config for all providers. + This config is no longer used anywhere, and adopters should use [`integrations` config](https://backstage.io/docs/integrations) instead. + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- 3e59f90b51: Fix error handling of the `runCommand` helper to return `Error` + instance. +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 216725b434: Updated to use new names for `parseLocationRef` and `stringifyLocationRef` +- e72d371296: Use `TemplateEntityV1beta2` from `@backstage/plugin-scaffolder-common` instead + of `@backstage/catalog-model`. +- 1433045c08: Removed unused `helmet` dependency. +- 27eccab216: Replaces use of deprecated catalog-model constants. +- Updated dependencies + - @backstage/plugin-scaffolder-common@0.2.0 + - @backstage/plugin-catalog-backend@0.21.4 + - @backstage/backend-common@0.10.8 + - @backstage/catalog-client@0.7.0 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.0 + ## 0.15.24 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 5e7b5a428b..3bc03c4840 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -18,8 +18,9 @@ import { GithubCredentialsProvider } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; -import { LocationSpec } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger as Logger_2 } from 'winston'; +import { Observable } from '@backstage/types'; import { Octokit } from 'octokit'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { Schema } from 'jsonschema'; @@ -27,192 +28,271 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { TaskSpecV1beta2 } from '@backstage/plugin-scaffolder-common'; -import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { TemplateMetadata } from '@backstage/plugin-scaffolder-common'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; -// Warning: (ae-forgotten-export) The symbol "InputBase" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ActionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ActionContext = { +// @public +export type ActionContext = { baseUrl?: string; logger: Logger_2; logStream: Writable; - token?: string | undefined; secrets?: TaskSecrets; workspacePath: string; input: Input; output(name: string, value: JsonValue): void; createTemporaryDirectory(): Promise; metadata?: TemplateMetadata; + templateInfo?: TemplateInfo; }; -// Warning: (ae-missing-release-tag) "CatalogEntityClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public @deprecated +export type CompletedTaskState = TaskCompletionState; + // @public -export class CatalogEntityClient { - constructor(catalogClient: CatalogApi); - findTemplate( - templateName: string, - options?: { - token?: string; - }, - ): Promise; +export const createBuiltinActions: ( + options: CreateBuiltInActionsOptions, +) => TemplateAction[]; + +// @public +export interface CreateBuiltInActionsOptions { + // (undocumented) + additionalTemplateFilters?: Record; + // (undocumented) + catalogClient: CatalogApi; + // (undocumented) + config: Config; + // @deprecated (undocumented) + containerRunner?: ContainerRunner; + // (undocumented) + integrations: ScmIntegrations; + // (undocumented) + reader: UrlReader; } // @public -export type CompletedTaskState = 'failed' | 'completed'; - -// Warning: (ae-missing-release-tag) "createBuiltinActions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const createBuiltinActions: (options: { - reader: UrlReader; - integrations: ScmIntegrations; - catalogClient: CatalogApi; - containerRunner?: ContainerRunner; - config: Config; - additionalTemplateFilters?: Record; -}) => TemplateAction[]; - -// Warning: (ae-missing-release-tag) "createCatalogRegisterAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) export function createCatalogRegisterAction(options: { catalogClient: CatalogApi; integrations: ScmIntegrations; -}): TemplateAction; +}): TemplateAction< + | { + catalogInfoUrl: string; + optional?: boolean | undefined; + } + | { + repoContentsUrl: string; + catalogInfoPath?: string | undefined; + optional?: boolean | undefined; + } +>; -// Warning: (ae-missing-release-tag) "createCatalogWriteAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function createCatalogWriteAction(): TemplateAction; - -// Warning: (ae-missing-release-tag) "createDebugLogAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export function createDebugLogAction(): TemplateAction; +export function createCatalogWriteAction(): TemplateAction<{ + filePath?: string | undefined; + entity: Entity; +}>; + +// @public +export function createDebugLogAction(): TemplateAction<{ + message?: string | undefined; + listWorkspace?: boolean | undefined; +}>; export { createFetchCookiecutterAction }; -// Warning: (ae-missing-release-tag) "createFetchPlainAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createFetchPlainAction(options: { reader: UrlReader; integrations: ScmIntegrations; -}): TemplateAction; +}): TemplateAction<{ + url: string; + targetPath?: string | undefined; +}>; -// Warning: (ae-missing-release-tag) "createFetchTemplateAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createFetchTemplateAction(options: { reader: UrlReader; integrations: ScmIntegrations; additionalTemplateFilters?: Record; -}): TemplateAction; +}): TemplateAction<{ + url: string; + targetPath?: string | undefined; + values: any; + templateFileExtension?: string | boolean | undefined; + copyWithoutRender?: string[] | undefined; + cookiecutterCompat?: boolean | undefined; +}>; -// Warning: (ae-missing-release-tag) "createFilesystemDeleteAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const createFilesystemDeleteAction: () => TemplateAction; +// @public +export const createFilesystemDeleteAction: () => TemplateAction<{ + files: string[]; +}>; -// Warning: (ae-missing-release-tag) "createFilesystemRenameAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const createFilesystemRenameAction: () => TemplateAction; +// @public +export const createFilesystemRenameAction: () => TemplateAction<{ + files: Array<{ + from: string; + to: string; + overwrite?: boolean; + }>; +}>; -// Warning: (ae-missing-release-tag) "createGithubActionsDispatchAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createGithubActionsDispatchAction(options: { integrations: ScmIntegrations; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction; +}): TemplateAction<{ + repoUrl: string; + workflowId: string; + branchOrTagName: string; + workflowInputs?: + | { + [key: string]: string; + } + | undefined; + token?: string | undefined; +}>; -// Warning: (ae-missing-release-tag) "createGithubWebhookAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) +export interface CreateGithubPullRequestActionOptions { + // (undocumented) + clientFactory?: ( + input: CreateGithubPullRequestClientFactoryInput, + ) => Promise; + // (undocumented) + githubCredentialsProvider?: GithubCredentialsProvider; + // (undocumented) + integrations: ScmIntegrationRegistry; +} + +// @public (undocumented) +export type CreateGithubPullRequestClientFactoryInput = { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; + host: string; + owner: string; + repo: string; + token?: string; +}; + +// @public export function createGithubWebhookAction(options: { integrations: ScmIntegrationRegistry; defaultWebhookSecret?: string; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction; +}): TemplateAction<{ + repoUrl: string; + webhookUrl: string; + webhookSecret?: string | undefined; + events?: string[] | undefined; + active?: boolean | undefined; + contentType?: 'form' | 'json' | undefined; + insecureSsl?: boolean | undefined; + token?: string | undefined; +}>; -// Warning: (ae-missing-release-tag) "createPublishAzureAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createPublishAzureAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction; +}): TemplateAction<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + sourcePath?: string | undefined; + token?: string | undefined; +}>; -// Warning: (ae-missing-release-tag) "createPublishBitbucketAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction; +}): TemplateAction<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + repoVisibility?: 'private' | 'public' | undefined; + sourcePath?: string | undefined; + enableLFS?: boolean | undefined; + token?: string | undefined; +}>; -// Warning: (ae-missing-release-tag) "createPublishFileAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export function createPublishFileAction(): TemplateAction; +export function createPublishFileAction(): TemplateAction<{ + path: string; +}>; -// Warning: (ae-missing-release-tag) "createPublishGithubAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction; +}): TemplateAction<{ + repoUrl: string; + description?: string | undefined; + access?: string | undefined; + defaultBranch?: string | undefined; + sourcePath?: string | undefined; + requireCodeOwnerReviews?: boolean | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + collaborators?: + | { + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + }[] + | undefined; + token?: string | undefined; + topics?: string[] | undefined; +}>; -// Warning: (ae-forgotten-export) The symbol "CreateGithubPullRequestActionOptions" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createPublishGithubPullRequestAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const createPublishGithubPullRequestAction: ({ integrations, githubCredentialsProvider, clientFactory, -}: CreateGithubPullRequestActionOptions) => TemplateAction; +}: CreateGithubPullRequestActionOptions) => TemplateAction<{ + title: string; + branchName: string; + description: string; + repoUrl: string; + targetPath?: string | undefined; + sourcePath?: string | undefined; + token?: string | undefined; +}>; -// Warning: (ae-missing-release-tag) "createPublishGitlabAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createPublishGitlabAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction; +}): TemplateAction<{ + repoUrl: string; + defaultBranch?: string | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + sourcePath?: string | undefined; + token?: string | undefined; +}>; -// Warning: (ae-missing-release-tag) "createPublishGitlabMergeRequestAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const createPublishGitlabMergeRequestAction: (options: { integrations: ScmIntegrationRegistry; -}) => TemplateAction; +}) => TemplateAction<{ + projectid: string; + repoUrl: string; + title: string; + description: string; + branchName: string; + targetPath: string; + token?: string | undefined; +}>; -// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function createRouter(options: RouterOptions): Promise; -// Warning: (ae-missing-release-tag) "createTemplateAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const createTemplateAction: < - Input extends Partial<{ - [name: string]: JsonValue | Partial | undefined; - }>, ->( - templateAction: TemplateAction, -) => TemplateAction; +// @public +export const createTemplateAction: ( + templateAction: TemplateAction, +) => TemplateAction; // @public export type CreateWorkerOptions = { @@ -224,9 +304,18 @@ export type CreateWorkerOptions = { additionalTemplateFilters?: Record; }; +// @public +export interface CurrentClaimedTask { + // (undocumented) + secrets?: TaskSecrets; + // (undocumented) + spec: TaskSpec; + // (undocumented) + taskId: string; +} + // @public export class DatabaseTaskStore implements TaskStore { - constructor(options: DatabaseTaskStoreOptions); // (undocumented) claimTask(): Promise; // (undocumented) @@ -236,22 +325,23 @@ export class DatabaseTaskStore implements TaskStore { eventBody, }: { taskId: string; - status: Status; + status: TaskStatus; eventBody: JsonObject; }): Promise; - // Warning: (ae-forgotten-export) The symbol "DatabaseTaskStoreOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) static create(options: DatabaseTaskStoreOptions): Promise; // (undocumented) createTask( - spec: TaskSpec, - secrets?: TaskSecrets, - ): Promise<{ - taskId: string; - }>; + options: TaskStoreCreateTaskOptions, + ): Promise; // (undocumented) - emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; + emitLogEvent( + options: TaskStoreEmitOptions< + { + message: string; + } & JsonObject + >, + ): Promise; // (undocumented) getTask(taskId: string): Promise; // (undocumented) @@ -269,13 +359,17 @@ export class DatabaseTaskStore implements TaskStore { } // @public -export type DispatchResult = { - taskId: string; +export type DatabaseTaskStoreOptions = { + database: Knex; }; -// Warning: (ae-missing-release-tag) "fetchContents" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated +export type DispatchResult = TaskBrokerDispatchResult; + +// @public +export const executeShellCommand: (options: RunCommandOptions) => Promise; + +// @public export function fetchContents({ reader, integrations, @@ -286,19 +380,21 @@ export function fetchContents({ reader: UrlReader; integrations: ScmIntegrations; baseUrl?: string; - fetchUrl?: JsonValue; + fetchUrl?: string; outputPath: string; }): Promise; // Warning: (ae-missing-release-tag) "OctokitProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public +// @public @deprecated export class OctokitProvider { constructor( integrations: ScmIntegrationRegistry, githubCredentialsProvider?: GithubCredentialsProvider, ); // Warning: (ae-forgotten-export) The symbol "OctokitIntegration" needs to be exported by the entry point index.d.ts + // + // @deprecated getOctokit( repoUrl: string, options?: { @@ -307,6 +403,16 @@ export class OctokitProvider { ): Promise; } +// @public (undocumented) +export interface OctokitWithPullRequestPluginClient { + // (undocumented) + createPullRequest(options: createPullRequest.Options): Promise<{ + data: { + html_url: string; + }; + } | null>; +} + // @public export interface RouterOptions { // (undocumented) @@ -331,19 +437,21 @@ export interface RouterOptions { taskWorkers?: number; } -// Warning: (ae-forgotten-export) The symbol "RunCommandOptions" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "runCommand" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export const runCommand: ({ - command, - args, - logStream, - options, -}: RunCommandOptions) => Promise; +// @public @deprecated +export const runCommand: (options: RunCommandOptions) => Promise; + +// @public (undocumented) +export type RunCommandOptions = { + command: string; + args: string[]; + options?: SpawnOptionsWithoutStdio; + logStream?: Writable; +}; // @public (undocumented) export class ScaffolderEntitiesProcessor implements CatalogProcessor { + // (undocumented) + getProcessorName(): string; // (undocumented) postProcessEntity( entity: Entity, @@ -358,7 +466,7 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { export type SerializedTask = { id: string; spec: TaskSpec; - status: Status; + status: TaskStatus; createdAt: string; lastHeartbeatAt?: string; secrets?: TaskSecrets; @@ -373,49 +481,49 @@ export type SerializedTaskEvent = { createdAt: string; }; -// @public -export type Status = - | 'open' - | 'processing' - | 'failed' - | 'cancelled' - | 'completed'; +// @public @deprecated +export type Status = TaskStatus; // @public export interface TaskBroker { // (undocumented) claim(): Promise; // (undocumented) - dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; + dispatch( + options: TaskBrokerDispatchOptions, + ): Promise; + // (undocumented) + event$(options: { taskId: string; after: number | undefined }): Observable<{ + events: SerializedTaskEvent[]; + }>; // (undocumented) get(taskId: string): Promise; // (undocumented) - observe( - options: { - taskId: string; - after: number | undefined; - }, - callback: ( - error: Error | undefined, - result: { - events: SerializedTaskEvent[]; - }, - ) => void, - ): { - unsubscribe: () => void; - }; - // (undocumented) - vacuumTasks(timeoutS: { timeoutS: number }): Promise; + vacuumTasks(options: { timeoutS: number }): Promise; } +// @public +export type TaskBrokerDispatchOptions = { + spec: TaskSpec; + secrets?: TaskSecrets; +}; + +// @public +export type TaskBrokerDispatchResult = { + taskId: string; +}; + +// @public +export type TaskCompletionState = 'failed' | 'completed'; + // @public export interface TaskContext { // (undocumented) - complete(result: CompletedTaskState, metadata?: JsonValue): Promise; + complete(result: TaskCompletionState, metadata?: JsonObject): Promise; // (undocumented) done: boolean; // (undocumented) - emitLog(message: string, metadata?: JsonValue): Promise; + emitLog(message: string, logMetadata?: JsonObject): Promise; // (undocumented) getWorkspaceName(): Promise; // (undocumented) @@ -430,17 +538,17 @@ export type TaskEventType = 'completion' | 'log'; // @public export class TaskManager implements TaskContext { // (undocumented) - complete(result: CompletedTaskState, metadata?: JsonObject): Promise; + complete(result: TaskCompletionState, metadata?: JsonObject): Promise; // (undocumented) static create( - state: TaskState, + task: CurrentClaimedTask, storage: TaskStore, logger: Logger_2, ): TaskManager; // (undocumented) get done(): boolean; // (undocumented) - emitLog(message: string, metadata?: JsonObject): Promise; + emitLog(message: string, logMetadata?: JsonObject): Promise; // (undocumented) getWorkspaceName(): Promise; // (undocumented) @@ -451,25 +559,19 @@ export class TaskManager implements TaskContext { // @public export type TaskSecrets = Record & { - token?: string; backstageToken?: string; }; -export { TaskSpec }; - -export { TaskSpecV1beta2 }; - -export { TaskSpecV1beta3 }; +// @public @deprecated +export type TaskState = CurrentClaimedTask; // @public -export interface TaskState { - // (undocumented) - secrets?: TaskSecrets; - // (undocumented) - spec: TaskSpec; - // (undocumented) - taskId: string; -} +export type TaskStatus = + | 'open' + | 'processing' + | 'failed' + | 'cancelled' + | 'completed'; // @public export interface TaskStore { @@ -478,16 +580,13 @@ export interface TaskStore { // (undocumented) completeTask(options: { taskId: string; - status: Status; + status: TaskStatus; eventBody: JsonObject; }): Promise; // (undocumented) createTask( - task: TaskSpec, - secrets?: TaskSecrets, - ): Promise<{ - taskId: string; - }>; + options: TaskStoreCreateTaskOptions, + ): Promise; // (undocumented) emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; // (undocumented) @@ -507,9 +606,20 @@ export interface TaskStore { } // @public -export type TaskStoreEmitOptions = { +export type TaskStoreCreateTaskOptions = { + spec: TaskSpec; + secrets?: TaskSecrets; +}; + +// @public +export type TaskStoreCreateTaskResult = { taskId: string; - body: JsonObject; +}; + +// @public +export type TaskStoreEmitOptions = { + taskId: string; + body: TBody; }; // @public @@ -528,10 +638,8 @@ export class TaskWorker { start(): void; } -// Warning: (ae-missing-release-tag) "TemplateAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export type TemplateAction = { +export type TemplateAction = { id: string; description?: string; schema?: { @@ -541,24 +649,16 @@ export type TemplateAction = { handler: (ctx: ActionContext) => Promise; }; -// Warning: (ae-missing-release-tag) "TemplateActionRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class TemplateActionRegistry { // (undocumented) - get(actionId: string): TemplateAction; + get(actionId: string): TemplateAction; // (undocumented) - list(): TemplateAction[]; + list(): TemplateAction[]; // (undocumented) - register( - action: TemplateAction, - ): void; + register(action: TemplateAction): void; } -// Warning: (ae-missing-release-tag) "TemplateFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined; - -export { TemplateMetadata }; ``` diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index aa4ecf8053..7883284165 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -28,30 +28,5 @@ export interface Config { * The commit message used when new components are created. */ defaultCommitMessage?: string; - /** - * @deprecated Replaced by parameters for the publish:github action - */ - github?: { - [key: string]: string; - }; - /** - * @deprecated Use the Gitlab integration instead - */ - gitlab?: { - api: { [key: string]: string }; - }; - /** - * @deprecated Use the Azure integration instead - */ - azure?: { - baseUrl: string; - api: { [key: string]: string }; - }; - /** - * @deprecated Use the Bitbucket integration instead - */ - bitbucket?: { - api: { [key: string]: string }; - }; }; } diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 3161448562..51ff17c1e2 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.24", + "version": "0.17.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,26 +24,26 @@ "backstage" ], "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", + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", "build:assets": "node scripts/build-nunjucks.js" }, "dependencies": { - "@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.3", - "@backstage/plugin-scaffolder-common": "^0.1.3", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.11", - "@backstage/types": "^0.1.1", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-scaffolder-common": "^0.2.2", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.2.2", + "@backstage/types": "^0.1.3", "@gitbeaker/core": "^34.6.0", "@gitbeaker/node": "^35.1.0", "@octokit/webhooks": "^9.14.1", @@ -55,7 +58,6 @@ "git-url-parse": "^11.6.0", "globby": "^11.0.0", "handlebars": "^4.7.6", - "helmet": "^4.0.0", "isbinaryfile": "^4.0.8", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", @@ -70,17 +72,19 @@ "uuid": "^8.2.0", "winston": "^3.2.1", "yaml": "^1.10.0", - "vm2": "^3.9.6" + "vm2": "^3.9.6", + "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/test-utils": "^0.2.6", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", "@types/mock-fs": "^4.13.0", "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", + "@types/zen-observable": "^0.8.0", "esbuild": "^0.14.1", "jest-when": "^3.1.0", "mock-fs": "^5.1.0", diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts deleted file mode 100644 index 430afa5b5d..0000000000 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ /dev/null @@ -1,58 +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 { TemplateEntityV1beta2 } from '@backstage/catalog-model'; -import { CatalogApi } from '@backstage/catalog-client'; -import { ConflictError, NotFoundError } from '@backstage/errors'; - -/** - * A catalog client tailored for reading out entity data from the catalog. - */ -export class CatalogEntityClient { - constructor(private readonly catalogClient: CatalogApi) {} - - /** - * Looks up a single template using a template name. - * - * Throws a NotFoundError or ConflictError if 0 or multiple templates are found. - */ - async findTemplate( - templateName: string, - options?: { token?: string }, - ): Promise { - const { items: templates } = (await this.catalogClient.getEntities( - { - filter: { - kind: 'template', - 'metadata.name': templateName, - }, - }, - options, - )) as { items: TemplateEntityV1beta2[] }; - - if (templates.length !== 1) { - if (templates.length > 1) { - throw new ConflictError( - 'Templates lookup resulted in multiple matches', - ); - } else { - throw new NotFoundError('Template not found'); - } - } - - return templates[0]; - } -} diff --git a/plugins/scaffolder-backend/src/lib/index.ts b/plugins/scaffolder-backend/src/lib/index.ts index dbb4348a2c..20096f9c03 100644 --- a/plugins/scaffolder-backend/src/lib/index.ts +++ b/plugins/scaffolder-backend/src/lib/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export * from './catalog'; export * from './templating'; diff --git a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts index 6d3ce6f556..e82620a90d 100644 --- a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts +++ b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts @@ -95,6 +95,7 @@ const { render, renderCompat } = (() => { })(); `; +/** @public */ export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined; export interface SecureTemplaterOptions { diff --git a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts index 4f3c13f342..5fcd89841e 100644 --- a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts +++ b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts @@ -17,31 +17,32 @@ import { Entity, getEntityName, - LocationSpec, parseEntityRef, RELATION_OWNED_BY, RELATION_OWNER_OF, - entityKindSchemaValidator, } from '@backstage/catalog-model'; import { CatalogProcessor, CatalogProcessorEmit, - results, + LocationSpec, + processingResult, } from '@backstage/plugin-catalog-backend'; import { TemplateEntityV1beta3, - templateEntityV1beta3Schema, + templateEntityV1beta3Validator, } from '@backstage/plugin-scaffolder-common'; /** @public */ export class ScaffolderEntitiesProcessor implements CatalogProcessor { - private readonly validators = [ - entityKindSchemaValidator(templateEntityV1beta3Schema), - ]; + getProcessorName(): string { + return 'ScaffolderEntitiesProcessor'; + } + + private readonly validators = [templateEntityV1beta3Validator]; async validateEntityKind(entity: Entity): Promise { for (const validator of this.validators) { - if (validator(entity)) { + if (await validator.check(entity)) { return true; } } @@ -69,7 +70,7 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { defaultNamespace: selfRef.namespace, }); emit( - results.relation({ + processingResult.relation({ source: selfRef, type: RELATION_OWNED_BY, target: { @@ -80,7 +81,7 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { }), ); emit( - results.relation({ + processingResult.relation({ source: { kind: targetRef.kind, namespace: targetRef.namespace, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts index cd31db49eb..8a2a00d121 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts @@ -14,13 +14,18 @@ * limitations under the License. */ -import { InputBase, TemplateAction } from './types'; +import { JsonObject } from '@backstage/types'; import { ConflictError, NotFoundError } from '@backstage/errors'; +import { TemplateAction } from './types'; +/** + * Registry of all registered template actions. + * @public + */ export class TemplateActionRegistry { private readonly actions = new Map>(); - register(action: TemplateAction) { + register(action: TemplateAction) { if (this.actions.has(action.id)) { throw new ConflictError( `Template action with ID '${action.id}' has already been registered`, @@ -29,7 +34,7 @@ export class TemplateActionRegistry { this.actions.set(action.id, action); } - get(actionId: string): TemplateAction { + get(actionId: string): TemplateAction { const action = this.actions.get(actionId); if (!action) { throw new NotFoundError( @@ -39,7 +44,7 @@ export class TemplateActionRegistry { return action; } - list(): TemplateAction[] { + list(): TemplateAction[] { return [...this.actions.values()]; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts index 65d0e1b1e7..1b30268d9d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -20,6 +20,10 @@ import { CatalogApi } from '@backstage/catalog-client'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { createTemplateAction } from '../../createTemplateAction'; +/** + * Registers entities from a catalog descriptor file in the workspace into the software catalog. + * @public + */ export function createCatalogRegisterAction(options: { catalogClient: CatalogApi; integrations: ScmIntegrations; 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/catalog/write.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts index 9b159f1611..b55e521784 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -20,6 +20,10 @@ import * as yaml from 'yaml'; import { Entity } from '@backstage/catalog-model'; import { resolveSafeChildPath } from '@backstage/backend-common'; +/** + * Writes a catalog descriptor file containing the provided entity to a path in the workspace. + * @public + */ export function createCatalogWriteAction() { return createTemplateAction<{ filePath?: string; entity: Entity }>({ id: 'catalog:write', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 481ab894ec..c03ba150c2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -15,6 +15,7 @@ */ import { ContainerRunner, UrlReader } from '@backstage/backend-common'; +import { JsonObject } from '@backstage/types'; import { CatalogApi } from '@backstage/catalog-client'; import { GithubCredentialsProvider, @@ -47,15 +48,33 @@ import { createGithubWebhookAction, } from './github'; import { TemplateFilter } from '../../../lib'; +import { TemplateAction } from '../types'; +import { getRootLogger } from '@backstage/backend-common'; -export const createBuiltinActions = (options: { +/** + * The options passed to {@link createBuiltinActions} + * @public + */ +export interface CreateBuiltInActionsOptions { reader: UrlReader; integrations: ScmIntegrations; catalogClient: CatalogApi; + /** @deprecated when the cookiecutter action is removed this won't be necessary */ containerRunner?: ContainerRunner; config: Config; additionalTemplateFilters?: Record; -}) => { +} + +/** + * A function to generate create a list of default actions that the scaffolder provides. + * Is called internally in the default setup, but can be used when adding your own actions or overriding the default ones + * + * @public + * @returns A list of actions that can be used in the scaffolder + */ +export const createBuiltinActions = ( + options: CreateBuiltInActionsOptions, +): TemplateAction[] => { const { reader, integrations, @@ -117,6 +136,11 @@ export const createBuiltinActions = (options: { ]; if (containerRunner) { + getRootLogger().warn( + `[DEPRECATED] The fetch:cookiecutter action will be removed part of the default scaffolder actions in later versions. +You can install the package seperately and remove the containerRunner from the createBuiltInActions to remove this warning, +or you can migrate to using fetch:template https://backstage.io/docs/features/software-templates/builtin-actions#migrating-from-fetchcookiecutter-to-fetchtemplate`, + ); actions.push( createFetchCookiecutterAction({ reader, @@ -126,5 +150,5 @@ export const createBuiltinActions = (options: { ); } - return actions; + return actions as TemplateAction[]; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts index 38c50afecc..ecd0f33e21 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts @@ -61,7 +61,7 @@ describe('debug:log', () => { const context = { ...mockContext, input: { - listWorkspace: 'true', + listWorkspace: true, }, }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts index 36ea842a66..59d4c333c6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -19,8 +19,14 @@ import { relative, join } from 'path'; import { createTemplateAction } from '../../createTemplateAction'; /** + * Writes a message into the log or lists all files in the workspace + * + * @remarks + * * This task is useful for local development and testing of both the scaffolder * and scaffolder templates. + * + * @public */ export function createDebugLogAction() { return createTemplateAction<{ message?: string; listWorkspace?: boolean }>({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts index a738afbe3f..d274ad3ba8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts @@ -50,15 +50,6 @@ describe('fetchContent helper', () => { outputPath: os.tmpdir(), }; - it('should reject non string fetchUrls', async () => { - await expect( - fetchContents({ - ...options, - fetchUrl: false, - }), - ).rejects.toThrow('Invalid url parameter, expected string, got boolean'); - }); - it('should reject absolute file locations', async () => { await expect( fetchContents({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts index b56bab0d53..a01fcee6f7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts @@ -15,12 +15,17 @@ */ import { resolveSafeChildPath, UrlReader } from '@backstage/backend-common'; -import { JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import fs from 'fs-extra'; import path from 'path'; +/** + * A helper function that reads the contents of a directory from the given URL. + * Can be used in your own actions, and also used behind fetch:template and fetch:plain + * + * @public + */ export async function fetchContents({ reader, integrations, @@ -31,15 +36,9 @@ export async function fetchContents({ reader: UrlReader; integrations: ScmIntegrations; baseUrl?: string; - fetchUrl?: JsonValue; + fetchUrl?: string; outputPath: string; }) { - if (typeof fetchUrl !== 'string') { - throw new InputError( - `Invalid url parameter, expected string, got ${typeof fetchUrl}`, - ); - } - let fetchUrlIsAbsolute = false; try { // eslint-disable-next-line no-new diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts index 9cac6d5ab8..6dbe08f61b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts @@ -19,6 +19,11 @@ import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from './helpers'; import { createTemplateAction } from '../../createTemplateAction'; +/** + * Downloads content and places it in the workspace, or optionally + * in a subdirectory specified by the 'targetPath' input option. + * @public + */ export function createFetchPlainAction(options: { reader: UrlReader; integrations: ScmIntegrations; @@ -59,7 +64,7 @@ export function createFetchPlainAction(options: { await fetchContents({ reader, integrations, - baseUrl: ctx.baseUrl, + baseUrl: ctx.templateInfo?.baseUrl, fetchUrl: ctx.input.url, outputPath, }); 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 6ee8f8ce01..8f641fe2e2 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 @@ -27,12 +27,18 @@ import { ScmIntegrations } from '@backstage/integration'; import { PassThrough } from 'stream'; import { fetchContents } from './helpers'; import { ActionContext, TemplateAction } from '../../types'; -import { createFetchTemplateAction, FetchTemplateInput } from './template'; +import { createFetchTemplateAction } from './template'; jest.mock('./helpers', () => ({ fetchContents: jest.fn(), })); +type FetchTemplateInput = ReturnType< + typeof createFetchTemplateAction +> extends TemplateAction + ? U + : never; + const realFiles = Object.fromEntries( [ resolvePackagePath( @@ -69,7 +75,10 @@ describe('fetch:template', () => { const logger = getVoidLogger(); const mockContext = (inputPatch: Partial = {}) => ({ - baseUrl: 'base-url', + templateInfo: { + baseUrl: 'base-url', + entityRef: 'template:default/test-template', + }, input: { url: './skeleton', targetPath: './target', @@ -193,7 +202,7 @@ describe('fetch:template', () => { it('uses fetchContents to retrieve the template content', () => { expect(mockFetchContents).toHaveBeenCalledWith( expect.objectContaining({ - baseUrl: context.baseUrl, + baseUrl: context.templateInfo?.baseUrl, fetchUrl: context.input.url, }), ); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts index 32a3a84070..3d3d319c57 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -28,22 +28,13 @@ import { SecureTemplater, } from '../../../../lib/templating/SecureTemplater'; -type CookieCompatInput = { - copyWithoutRender?: string[]; - cookiecutterCompat?: boolean; -}; - -type ExtensionInput = { - templateFileExtension?: string | boolean; -}; - -export type FetchTemplateInput = { - url: string; - targetPath?: string; - values: any; -} & CookieCompatInput & - ExtensionInput; - +/** + * Downloads a skeleton, templates variables into file and directory names and content. + * Then places the result in the workspace, or optionally in a subdirectory + * specified by the 'targetPath' input option. + * + * @public + */ export function createFetchTemplateAction(options: { reader: UrlReader; integrations: ScmIntegrations; @@ -51,7 +42,16 @@ export function createFetchTemplateAction(options: { }) { const { reader, integrations, additionalTemplateFilters } = options; - return createTemplateAction({ + return createTemplateAction<{ + url: string; + targetPath?: string; + values: any; + templateFileExtension?: string | boolean; + + // Cookiecutter compat options + copyWithoutRender?: string[]; + cookiecutterCompat?: boolean; + }>({ id: 'fetch:template', description: "Downloads a skeleton, templates variables into file and directory names and content, and places the result in the workspace, or optionally in a subdirectory specified by the 'targetPath' input option.", @@ -142,7 +142,7 @@ export function createFetchTemplateAction(options: { await fetchContents({ reader, integrations, - baseUrl: ctx.baseUrl, + baseUrl: ctx.templateInfo?.baseUrl, fetchUrl: ctx.input.url, outputPath: templateDir, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts index ba8a145721..713de5b0ad 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts @@ -61,28 +61,28 @@ describe('fs:delete', () => { await expect( action.handler({ ...mockContext, - input: { files: undefined }, + input: { files: undefined } as any, }), ).rejects.toThrow(/files must be an Array/); await expect( action.handler({ ...mockContext, - input: { files: {} }, + input: { files: {} } as any, }), ).rejects.toThrow(/files must be an Array/); await expect( action.handler({ ...mockContext, - input: { files: '' }, + input: { files: '' } as any, }), ).rejects.toThrow(/files must be an Array/); await expect( action.handler({ ...mockContext, - input: { files: null }, + input: { files: null } as any, }), ).rejects.toThrow(/files must be an Array/); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts index 068f777ec5..bf2953e75a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts @@ -18,6 +18,10 @@ import { InputError } from '@backstage/errors'; import { resolveSafeChildPath } from '@backstage/backend-common'; import fs from 'fs-extra'; +/** + * Creates new action that enables deletion of files and directories in the workspace. + * @public + */ export const createFilesystemDeleteAction = () => { return createTemplateAction<{ files: string[] }>({ id: 'fs:delete', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts index 8452735083..b12e4269c2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts @@ -76,28 +76,28 @@ describe('fs:rename', () => { await expect( action.handler({ ...mockContext, - input: { files: undefined }, + input: { files: undefined } as any, }), ).rejects.toThrow(/files must be an Array/); await expect( action.handler({ ...mockContext, - input: { files: {} }, + input: { files: {} } as any, }), ).rejects.toThrow(/files must be an Array/); await expect( action.handler({ ...mockContext, - input: { files: '' }, + input: { files: '' } as any, }), ).rejects.toThrow(/files must be an Array/); await expect( action.handler({ ...mockContext, - input: { files: null }, + input: { files: null } as any, }), ).rejects.toThrow(/files must be an Array/); }); @@ -106,21 +106,21 @@ describe('fs:rename', () => { await expect( action.handler({ ...mockContext, - input: { files: ['old.md'] }, + input: { files: ['old.md'] } as any, }), ).rejects.toThrow(/each file must have a from and to property/); await expect( action.handler({ ...mockContext, - input: { files: [{ from: 'old.md' }] }, + input: { files: [{ from: 'old.md' }] } as any, }), ).rejects.toThrow(/each file must have a from and to property/); await expect( action.handler({ ...mockContext, - input: { files: [{ to: 'new.md' }] }, + input: { files: [{ to: 'new.md' }] } as any, }), ).rejects.toThrow(/each file must have a from and to property/); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts index 4bdba96750..7c8dad7559 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts @@ -17,16 +17,20 @@ import { createTemplateAction } from '../../createTemplateAction'; import { resolveSafeChildPath } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; -import { JsonObject } from '@backstage/types'; import fs from 'fs-extra'; -interface FilesToRename extends JsonObject { - from: string; - to: string; -} - +/** + * Creates a new action that allows renames of files and directories in the workspace. + * @public + */ export const createFilesystemRenameAction = () => { - return createTemplateAction<{ files: FilesToRename }>({ + return createTemplateAction<{ + files: Array<{ + from: string; + to: string; + overwrite?: boolean; + }>; + }>({ id: 'fs:rename', description: 'Renames files and directories within the workspace', schema: { 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 3ac66a0023..2f3c43a6b8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts @@ -32,6 +32,9 @@ export type OctokitIntegration = { /** * OctokitProvider provides Octokit client based on ScmIntegrationsRegistry configuration. * OctokitProvider supports GitHub credentials caching out of the box. + * + * @deprecated we are no longer providing a way from the scaffolder to generate octokit instances. + * Implement your own if you're using this method from an external package, or use the internal `getOctokitOptions` function instead */ export class OctokitProvider { private readonly integrations: ScmIntegrationRegistry; @@ -51,6 +54,9 @@ export class OctokitProvider { * gets standard Octokit client based on repository URL. * * @param repoUrl - Repository URL + * + * @deprecated we are no longer providing a way from the scaffolder to generate octokit instances. + * Implement your own if you're using this method from an external package, or use the internal `getOctokitOptions` function instead */ async getOctokit( repoUrl: string, 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 0c8b4d4aab..b9d77559e0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts @@ -13,24 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { InputError } from '@backstage/errors'; import { - DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; +import { Octokit } from 'octokit'; import { createTemplateAction } from '../../createTemplateAction'; -import { OctokitProvider } from './OctokitProvider'; +import { parseRepoUrl } from '../publish/util'; +import { getOctokitOptions } from './helpers'; +/** + * Creates a new action that dispatches a GitHub Action workflow for a given branch or tag. + * @public + */ export function createGithubActionsDispatchAction(options: { integrations: ScmIntegrations; githubCredentialsProvider?: GithubCredentialsProvider; }) { const { integrations, githubCredentialsProvider } = options; - const octokitProvider = new OctokitProvider( - integrations, - githubCredentialsProvider || - DefaultGithubCredentialsProvider.fromIntegrations(integrations), - ); return createTemplateAction<{ repoUrl: string; @@ -90,9 +91,19 @@ export function createGithubActionsDispatchAction(options: { `Dispatching workflow ${workflowId} for repo ${repoUrl} on ${branchOrTagName}`, ); - const { client, owner, repo } = await octokitProvider.getOctokit( - repoUrl, - { token: providedToken }, + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const client = new Octokit( + await getOctokitOptions({ + integrations, + repoUrl, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + }), ); await client.rest.actions.createWorkflowDispatch({ 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 660c7793f2..4547ce4492 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts @@ -14,17 +14,20 @@ * limitations under the License. */ import { - DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; import { createTemplateAction } from '../../createTemplateAction'; -import { OctokitProvider } from './OctokitProvider'; import { emitterEventNames } from '@octokit/webhooks'; -import { assertError } from '@backstage/errors'; - -type ContentType = 'form' | 'json'; +import { assertError, InputError } from '@backstage/errors'; +import { Octokit } from 'octokit'; +import { getOctokitOptions } from './helpers'; +import { parseRepoUrl } from '../publish/util'; +/** + * Creates new action that creates a webhook for a repository on GitHub. + * @public + */ export function createGithubWebhookAction(options: { integrations: ScmIntegrationRegistry; defaultWebhookSecret?: string; @@ -32,11 +35,7 @@ export function createGithubWebhookAction(options: { }) { const { integrations, defaultWebhookSecret, githubCredentialsProvider } = options; - const octokitProvider = new OctokitProvider( - integrations, - githubCredentialsProvider ?? - DefaultGithubCredentialsProvider.fromIntegrations(integrations), - ); + const eventNames = emitterEventNames.filter(event => !event.includes('.')); return createTemplateAction<{ @@ -45,7 +44,7 @@ export function createGithubWebhookAction(options: { webhookSecret?: string; events?: string[]; active?: boolean; - contentType?: ContentType; + contentType?: 'form' | 'json'; insecureSsl?: boolean; token?: string; }>({ @@ -129,10 +128,19 @@ export function createGithubWebhookAction(options: { } = ctx.input; ctx.logger.info(`Creating webhook ${webhookUrl} for repo ${repoUrl}`); + const { owner, repo } = parseRepoUrl(repoUrl, integrations); - const { client, owner, repo } = await octokitProvider.getOctokit( - repoUrl, - { token: providedToken }, + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const client = new Octokit( + await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + repoUrl: repoUrl, + token: providedToken, + }), ); try { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts new file mode 100644 index 0000000000..29997e5843 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { InputError } from '@backstage/errors'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { OctokitOptions } from '@octokit/core/dist-types/types'; +import { parseRepoUrl } from '../publish/util'; + +const DEFAULT_TIMEOUT_MS = 60_000; + +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; + token?: string; + repoUrl: string; +}): Promise { + const { integrations, credentialsProvider, repoUrl, token } = options; + const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); + + const requestOptions = { + // set timeout to 60 seconds + timeout: DEFAULT_TIMEOUT_MS, + }; + + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + + const integrationConfig = integrations.github.byHost(host)?.config; + + if (!integrationConfig) { + throw new InputError(`No integration for host ${host}`); + } + + // short circuit the `githubCredentialsProvider` if there is a token provided by the caller already + if (token) { + return { + auth: token, + baseUrl: integrationConfig.apiBaseUrl, + previews: ['nebula-preview'], + request: requestOptions, + }; + } + + const githubCredentialsProvider = + credentialsProvider ?? + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + + // TODO(blam): Consider changing this API to take host and repo instead of repoUrl, as we end up parsing in this function + // and then parsing in the `getCredentials` function too the other side + const { token: credentialProviderToken } = + await githubCredentialsProvider.getCredentials({ + url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( + repo, + )}`, + }); + + if (!credentialProviderToken) { + throw new InputError( + `No token available for host: ${host}, with owner ${owner}, and repo ${repo}`, + ); + } + + return { + auth: credentialProviderToken, + baseUrl: integrationConfig.apiBaseUrl, + previews: ['nebula-preview'], + }; +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 92b578d385..79537ff8dc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -21,6 +21,7 @@ import { Git } from '@backstage/backend-common'; import { Octokit } from 'octokit'; import { assertError } from '@backstage/errors'; +/** @public */ export type RunCommandOptions = { /** command to run */ command: string; @@ -34,15 +35,18 @@ export type RunCommandOptions = { /** * Run a command in a sub-process, normally a shell command. + * + * @public */ -export const runCommand = async ({ - command, - args, - logStream = new PassThrough(), - options, -}: RunCommandOptions) => { +export const executeShellCommand = async (options: RunCommandOptions) => { + const { + command, + args, + options: spawnOptions, + logStream = new PassThrough(), + } = options; await new Promise((resolve, reject) => { - const process = spawn(command, args, options); + const process = spawn(command, args, spawnOptions); process.stdout.on('data', stream => { logStream.write(stream); @@ -58,13 +62,22 @@ 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(); }); }); }; +/** + * Run a command in a sub-process, normally a shell command. + * @public + * @deprecated use {@link executeShellCommand} instead + */ +export const runCommand = executeShellCommand; + export async function initRepoAndPush({ dir, remoteUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index 2265f9b4f7..8be1b62eaa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -15,12 +15,15 @@ */ export * from './catalog'; -export { createBuiltinActions } from './createBuiltinActions'; +export * from './createBuiltinActions'; export * from './debug'; export * from './fetch'; export * from './filesystem'; export * from './publish'; export * from './github'; +/** @deprecated please add this package to your own installation manually */ export { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; -export { runCommand } from './helpers'; + +export { runCommand, executeShellCommand } from './helpers'; +export type { RunCommandOptions } from './helpers'; 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 4f67a0e45f..7a8e963b5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -23,6 +23,11 @@ import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '../../createTemplateAction'; import { Config } from '@backstage/config'; +/** + * Creates a new action that initializes a git repository of the content in the workspace + * and publishes it to Azure. + * @public + */ export function createPublishAzureAction(options: { integrations: ScmIntegrationRegistry; config: Config; 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 3d14710f3e..96fd8184ca 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 @@ -51,7 +51,7 @@ describe('publish:bitbucket', () => { const mockContext = { input: { repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - repoVisibility: 'private', + repoVisibility: 'private' as const, }, workspacePath: 'lol', logger: getVoidLogger(), @@ -567,4 +567,47 @@ describe('publish:bitbucket', () => { 'https://bitbucket.org/workspace/repo/src/master', ); }); + + it('should call outputs with the correct urls with correct default branch', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://bitbucket.org/workspace/cloneurl', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://bitbucket.org/workspace/repo/src/main', + ); + }); }); 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 8ba8592642..665c54fb3e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -29,9 +29,11 @@ const createBitbucketCloudRepository = async (opts: { workspace: string; project: string; repo: string; - description: string; + description?: string; repoVisibility: 'private' | 'public'; + mainBranch: string; authorization: string; + apiBaseUrl: string; }) => { const { workspace, @@ -39,7 +41,9 @@ const createBitbucketCloudRepository = async (opts: { repo, description, repoVisibility, + mainBranch, authorization, + apiBaseUrl, } = opts; const options: RequestInit = { @@ -59,7 +63,7 @@ const createBitbucketCloudRepository = async (opts: { let response: Response; try { response = await fetch( - `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`, + `${apiBaseUrl}/repositories/${workspace}/${repo}`, options, ); } catch (e) { @@ -82,22 +86,21 @@ const createBitbucketCloudRepository = async (opts: { } } - // TODO use the urlReader to get the default branch - const repoContentsUrl = `${r.links.html.href}/src/master`; + // "mainbranch.name" cannot be set neither at create nor update of the repo + // the first pushed branch will be set as "main branch" then + const repoContentsUrl = `${r.links.html.href}/src/${mainBranch}`; return { remoteUrl, repoContentsUrl }; }; const createBitbucketServerRepository = async (opts: { - host: string; project: string; repo: string; - description: string; + description?: string; repoVisibility: 'private' | 'public'; authorization: string; - apiBaseUrl?: string; + apiBaseUrl: string; }) => { const { - host, project, repo, description, @@ -121,8 +124,7 @@ const createBitbucketServerRepository = async (opts: { }; try { - const baseUrl = apiBaseUrl ? apiBaseUrl : `https://${host}/rest/api/1.0`; - response = await fetch(`${baseUrl}/projects/${project}/repos`, options); + response = await fetch(`${apiBaseUrl}/projects/${project}/repos`, options); } catch (e) { throw new Error(`Unable to create repository, ${e}`); } @@ -192,6 +194,11 @@ const performEnableLFS = async (opts: { ); }; +/** + * Creates a new action that initializes a git repository of the content in the workspace + * and publishes it to Bitbucket. + * @public + */ export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; config: Config; @@ -200,11 +207,11 @@ export function createPublishBitbucketAction(options: { return createTemplateAction<{ repoUrl: string; - description: string; + description?: string; defaultBranch?: string; - repoVisibility: 'private' | 'public'; + repoVisibility?: 'private' | 'public'; sourcePath?: string; - enableLFS: boolean; + enableLFS?: boolean; token?: string; }>({ id: 'publish:bitbucket', @@ -306,7 +313,11 @@ export function createPublishBitbucketAction(options: { const authorization = getAuthorizationHeader( ctx.input.token - ? { host: integrationConfig.config.host, token: ctx.input.token } + ? { + host: integrationConfig.config.host, + apiBaseUrl: integrationConfig.config.apiBaseUrl, + token: ctx.input.token, + } : integrationConfig.config, ); @@ -319,11 +330,11 @@ export function createPublishBitbucketAction(options: { const { remoteUrl, repoContentsUrl } = await createMethod({ authorization, - host, workspace: workspace || '', project, repo, repoVisibility, + mainBranch: defaultBranch, description, apiBaseUrl, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/file.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/file.ts index d59c87d4fa..2d2cb85930 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/file.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/file.ts @@ -23,8 +23,12 @@ import { createTemplateAction } from '../../createTemplateAction'; * This task is useful for local development and testing of both the scaffolder * and scaffolder templates. * + * @remarks + * * This action is not installed by default and should not be installed in * production, as it writes the files to the local filesystem of the scaffolder. + * + * @public */ export function createPublishFileAction() { return createTemplateAction<{ path: string }>({ 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 5467ca651c..1e58635bbe 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 @@ -52,7 +52,7 @@ describe('publish:github', () => { input: { repoUrl: 'github.com?repo=repo&owner=owner', description: 'description', - repoVisibility: 'private', + repoVisibility: 'private' as const, access: 'owner/blam', }, workspacePath: 'lol', 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 139fad04d4..926a63915a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { - DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; @@ -22,26 +21,25 @@ import { enableBranchProtectionOnDefaultRepoBranch, initRepoAndPush, } from '../helpers'; -import { getRepoSourceDirectory } from './util'; +import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '../../createTemplateAction'; import { Config } from '@backstage/config'; -import { OctokitProvider } from '../github/OctokitProvider'; -import { assertError } from '@backstage/errors'; - -type Permission = 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; -type Collaborator = { access: Permission; username: string }; +import { assertError, InputError } from '@backstage/errors'; +import { getOctokitOptions } from '../github/helpers'; +import { Octokit } from 'octokit'; +/** + * Creates a new action that initializes a git repository of the content in the workspace + * and publishes it to GitHub. + * + * @public + */ export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; }) { const { integrations, config, githubCredentialsProvider } = options; - const octokitProvider = new OctokitProvider( - integrations, - githubCredentialsProvider || - DefaultGithubCredentialsProvider.fromIntegrations(integrations), - ); return createTemplateAction<{ repoUrl: string; @@ -50,8 +48,11 @@ export function createPublishGithubAction(options: { defaultBranch?: string; sourcePath?: string; requireCodeOwnerReviews?: boolean; - repoVisibility: 'private' | 'internal' | 'public'; - collaborators: Collaborator[]; + repoVisibility?: 'private' | 'internal' | 'public'; + collaborators?: Array<{ + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + }>; token?: string; topics?: string[]; }>({ @@ -160,10 +161,20 @@ export function createPublishGithubAction(options: { token: providedToken, } = ctx.input; - const { client, token, owner, repo } = await octokitProvider.getOctokit( + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, repoUrl, - { token: providedToken }, - ); + }); + + const client = new Octokit(octokitOptions); const user = await client.rest.users.getByUsername({ username: owner, @@ -253,7 +264,7 @@ export function createPublishGithubAction(options: { defaultBranch, auth: { username: 'x-access-token', - password: token, + password: octokitOptions.auth, }, logger: ctx.logger, commitMessage: config.getOptionalString( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index 20ea4b03da..9956f16003 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -26,20 +26,27 @@ import { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; import { ActionContext, TemplateAction } from '../../types'; import { - ClientFactoryInput, + CreateGithubPullRequestClientFactoryInput, createPublishGithubPullRequestAction, - GithubPullRequestActionInput, - PullRequestCreator, + OctokitWithPullRequestPluginClient, } from './githubPullRequest'; const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; const workspacePath = resolvePath(root, 'my-workspace'); +type GithubPullRequestActionInput = ReturnType< + typeof createPublishGithubPullRequestAction +> extends TemplateAction + ? U + : never; + describe('createPublishGithubPullRequestAction', () => { let instance: TemplateAction; - let fakeClient: PullRequestCreator; + let fakeClient: OctokitWithPullRequestPluginClient; - let clientFactory: (input: ClientFactoryInput) => Promise; + let clientFactory: ( + input: CreateGithubPullRequestClientFactoryInput, + ) => Promise; beforeEach(() => { const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); 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 80333c4966..2ce8e1d81e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -20,7 +20,6 @@ import { parseRepoUrl, isExecutable } from './util'; import { GithubCredentialsProvider, ScmIntegrationRegistry, - SingleInstanceGithubCredentialsProvider, } from '@backstage/integration'; import { zipObject } from 'lodash'; import { createTemplateAction } from '../../createTemplateAction'; @@ -29,36 +28,21 @@ import { InputError, CustomErrorBase } from '@backstage/errors'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; import globby from 'globby'; import { resolveSafeChildPath } from '@backstage/backend-common'; +import { getOctokitOptions } from '../github/helpers'; export type Encoding = 'utf-8' | 'base64'; class GithubResponseError extends CustomErrorBase {} -type CreatePullRequestResponse = { - data: { html_url: string }; -}; - -export interface PullRequestCreator { - createPullRequest( - options: createPullRequest.Options, - ): Promise; +/** @public */ +export interface OctokitWithPullRequestPluginClient { + createPullRequest(options: createPullRequest.Options): Promise<{ + data: { html_url: string }; + } | null>; } -export type PullRequestCreatorConstructor = ( - octokit: Octokit, -) => PullRequestCreator; - -export type GithubPullRequestActionInput = { - title: string; - branchName: string; - description: string; - repoUrl: string; - targetPath?: string; - sourcePath?: string; - token?: string; -}; - -export type ClientFactoryInput = { +/** @public */ +export type CreateGithubPullRequestClientFactoryInput = { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; host: string; @@ -74,55 +58,49 @@ export const defaultClientFactory = async ({ repo, host = 'github.com', token: providedToken, -}: ClientFactoryInput): Promise => { - const integrationConfig = integrations.github.byHost(host)?.config; +}: CreateGithubPullRequestClientFactoryInput): Promise => { + const [encodedHost, encodedOwner, encodedRepo] = [host, owner, repo].map( + encodeURIComponent, + ); + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + repoUrl: `${encodedHost}?owner=${encodedOwner}&repo=${encodedRepo}`, + token: providedToken, + }); + 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); - - const { token } = await credentialsProvider.getCredentials({ - url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( - repo, - )}`, - }); - - if (!token) { - throw new InputError( - `No token available for host: ${host}, with owner ${owner}, and repo ${repo}`, - ); - } - - return new OctokitPR({ - auth: token, - baseUrl: integrationConfig.apiBaseUrl, - }); + return new OctokitPR(octokitOptions); }; -interface CreateGithubPullRequestActionOptions { +/** @public */ +export interface CreateGithubPullRequestActionOptions { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; - clientFactory?: (input: ClientFactoryInput) => Promise; + clientFactory?: ( + input: CreateGithubPullRequestClientFactoryInput, + ) => Promise; } +/** + * Creates a Github Pull Request action. + * @public + */ export const createPublishGithubPullRequestAction = ({ integrations, githubCredentialsProvider, clientFactory = defaultClientFactory, }: CreateGithubPullRequestActionOptions) => { - return createTemplateAction({ + return createTemplateAction<{ + title: string; + branchName: string; + description: string; + repoUrl: string; + targetPath?: string; + sourcePath?: string; + token?: string; + }>({ id: 'publish:github:pull-request', schema: { input: { 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 a32fea8830..db4ad5a019 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 @@ -45,7 +45,7 @@ describe('publish:gitlab', () => { const mockContext = { input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', - repoVisibility: 'private', + repoVisibility: 'private' as const, }, workspacePath: 'lol', logger: getVoidLogger(), 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 48979306b1..9aa12803eb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -22,6 +22,12 @@ import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '../../createTemplateAction'; import { Config } from '@backstage/config'; +/** + * Creates a new action that initializes a git repository of the content in the workspace + * and publishes it to GitLab. + * + * @public + */ export function createPublishGitlabAction(options: { integrations: ScmIntegrationRegistry; config: Config; @@ -31,7 +37,7 @@ export function createPublishGitlabAction(options: { return createTemplateAction<{ repoUrl: string; defaultBranch?: string; - repoVisibility: 'private' | 'internal' | 'public'; + repoVisibility?: 'private' | 'internal' | 'public'; sourcePath?: string; token?: string; }>({ 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 bf687268c3..63cfeaa8b3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -24,22 +24,25 @@ import { InputError } from '@backstage/errors'; import { parseRepoUrl } from './util'; import { resolveSafeChildPath } from '@backstage/backend-common'; -export type GitlabMergeRequestActionInput = { - projectid: string; - repoUrl: string; - title: string; - description: string; - branchName: string; - targetPath: string; - token?: string; -}; - +/** + * Create a new action that creates a gitlab merge request. + * + * @public + */ export const createPublishGitlabMergeRequestAction = (options: { integrations: ScmIntegrationRegistry; }) => { const { integrations } = options; - return createTemplateAction({ + return createTemplateAction<{ + projectid: string; + repoUrl: string; + title: string; + description: string; + branchName: string; + targetPath: string; + token?: string; + }>({ id: 'publish:gitlab:merge-request', schema: { input: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts index c42b04e55e..7d85b45edd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts @@ -19,5 +19,10 @@ export { createPublishBitbucketAction } from './bitbucket'; export { createPublishFileAction } from './file'; export { createPublishGithubAction } from './github'; export { createPublishGithubPullRequestAction } from './githubPullRequest'; +export type { + CreateGithubPullRequestClientFactoryInput, + CreateGithubPullRequestActionOptions, + OctokitWithPullRequestPluginClient, +} from './githubPullRequest'; export { createPublishGitlabAction } from './gitlab'; export { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts b/plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts index a741900f9d..8893fa0f20 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts @@ -14,11 +14,16 @@ * limitations under the License. */ -import { InputBase, TemplateAction } from './types'; +import { JsonObject } from '@backstage/types'; +import { TemplateAction } from './types'; -export const createTemplateAction = ( - templateAction: TemplateAction, -): TemplateAction => { +/** + * This function is used to create new template actions to get type safety. + * @public + */ +export const createTemplateAction = ( + templateAction: TemplateAction, +): TemplateAction => { // TODO(blam): Can add some more validation here to validate the action later on return templateAction; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 4b842e8c5c..2a87f58fd8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -18,26 +18,24 @@ import { Logger } from 'winston'; import { Writable } from 'stream'; import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; -import { TaskSecrets, TemplateMetadata } from '../tasks/types'; +import { TaskSecrets } from '../tasks/types'; +import { + TemplateInfo, + TemplateMetadata, +} from '@backstage/plugin-scaffolder-common'; -type PartialJsonObject = Partial; -type PartialJsonValue = PartialJsonObject | JsonValue | undefined; -export type InputBase = Partial<{ [name: string]: PartialJsonValue }>; - -export type ActionContext = { +/** + * ActionContext is passed into scaffolder actions. + * @public */ +export type ActionContext = { /** * Base URL for the location of the task spec, typically the url of the source entity file. + * @deprecated please use templateInfo.baseUrl instead */ baseUrl?: string; logger: Logger; logStream: Writable; - - /** - * User token forwarded from initial request, for use in subsequent api requests - * @deprecated use `secrets.backstageToken` instead - */ - token?: string | undefined; secrets?: TaskSecrets; workspacePath: string; input: Input; @@ -48,10 +46,15 @@ export type ActionContext = { */ createTemporaryDirectory(): Promise; + /** + * @deprecated please use templateInfo instead + */ metadata?: TemplateMetadata; + templateInfo?: TemplateInfo; }; -export type TemplateAction = { +/** @public */ +export type TemplateAction = { id: string; description?: string; schema?: { diff --git a/plugins/scaffolder-backend/src/scaffolder/index.ts b/plugins/scaffolder-backend/src/scaffolder/index.ts index e055223d8c..0c6b5e01cc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/index.ts @@ -13,5 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export * from './actions'; export * from './tasks'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index dcda3b921e..65b890c265 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -22,13 +22,13 @@ import { v4 as uuid } from 'uuid'; import { SerializedTaskEvent, SerializedTask, - Status, + TaskStatus, TaskEventType, - TaskSecrets, - TaskSpec, TaskStore, TaskStoreEmitOptions, TaskStoreListEventsOptions, + TaskStoreCreateTaskOptions, + TaskStoreCreateTaskResult, } from './types'; import { DateTime } from 'luxon'; @@ -40,7 +40,7 @@ const migrationsDir = resolvePackagePath( export type RawDbTaskRow = { id: string; spec: string; - status: Status; + status: TaskStatus; last_heartbeat_at?: string; created_at: string; secrets?: string | null; @@ -80,7 +80,7 @@ export class DatabaseTaskStore implements TaskStore { return new DatabaseTaskStore(options); } - constructor(options: DatabaseTaskStoreOptions) { + private constructor(options: DatabaseTaskStoreOptions) { this.db = options.database; } @@ -108,14 +108,13 @@ export class DatabaseTaskStore implements TaskStore { } async createTask( - spec: TaskSpec, - secrets?: TaskSecrets, - ): Promise<{ taskId: string }> { + options: TaskStoreCreateTaskOptions, + ): Promise { const taskId = uuid(); await this.db('tasks').insert({ id: taskId, - spec: JSON.stringify(spec), - secrets: secrets ? JSON.stringify(secrets) : undefined, + spec: JSON.stringify(options.spec), + secrets: options.secrets ? JSON.stringify(options.secrets) : undefined, status: 'open', }); return { taskId }; @@ -202,7 +201,7 @@ export class DatabaseTaskStore implements TaskStore { eventBody, }: { taskId: string; - status: Status; + status: TaskStatus; eventBody: JsonObject; }): Promise { let oldStatus: string; @@ -253,12 +252,15 @@ export class DatabaseTaskStore implements TaskStore { }); } - async emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise { - const serliazedBody = JSON.stringify(body); + async emitLogEvent( + options: TaskStoreEmitOptions<{ message: string } & JsonObject>, + ): Promise { + const { taskId, body } = options; + const serializedBody = JSON.stringify(body); await this.db('task_events').insert({ task_id: taskId, event_type: 'log', - body: serliazedBody, + body: serializedBody, }); } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts index 9b05c43831..dcad1fc5d0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts @@ -21,8 +21,9 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; -import { TaskContext, TaskSpec } from './types'; +import { TaskContext } from './types'; import { RepoSpec } from '../actions/builtin/publish/util'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; describe('LegacyWorkflowRunner', () => { let runner: HandlebarsWorkflowRunner; @@ -94,7 +95,7 @@ describe('LegacyWorkflowRunner', () => { }); it('should pass metadata through', async () => { - const templateName = 'template name'; + const entityRef = `template:default/templateName`; const task = createMockTaskWithSpec({ apiVersion: 'backstage.io/v1beta2', steps: [ @@ -107,13 +108,13 @@ describe('LegacyWorkflowRunner', () => { ], output: {}, values: {}, - metadata: { name: templateName }, + templateInfo: { entityRef }, }); await runner.execute(task); - expect(fakeActionHandler.mock.calls[0][0].metadata).toEqual({ - name: templateName, + expect(fakeActionHandler.mock.calls[0][0].templateInfo).toEqual({ + entityRef, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts index 91c82edebb..c82da51ead 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts @@ -13,13 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - TaskContext, - WorkflowRunner, - WorkflowResponse, - TaskSpecV1beta2, - TaskSpec, -} from './types'; +import { TaskContext, WorkflowRunner, WorkflowResponse } from './types'; import * as Handlebars from 'handlebars'; import { TemplateActionRegistry } from '..'; import { ScmIntegrations } from '@backstage/integration'; @@ -33,6 +27,7 @@ import fs from 'fs-extra'; import { validate as validateJsonSchema } from 'jsonschema'; import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; +import { TaskSpec, TaskSpecV1beta2 } from '@backstage/plugin-scaffolder-common'; type Options = { workingDirectory: string; @@ -225,19 +220,12 @@ export class HandlebarsWorkflowRunner implements WorkflowRunner { input: JSON.stringify(input, null, 2), }); - if (!task.spec.metadata) { - console.warn( - 'DEPRECATION NOTICE: metadata is undefined. metadata will be required in the future.', - ); - } - await action.handler({ + // deprecated in favor of templateInfo.baseUrl baseUrl: task.spec.baseUrl, logger: taskLogger, logStream: stream, input, - // this token is deprecated, and will be removed in favour of secrets.backstageToken instead. - token: task.secrets?.token, secrets: task.secrets ?? {}, workspacePath, async createTemporaryDirectory() { @@ -250,7 +238,9 @@ export class HandlebarsWorkflowRunner implements WorkflowRunner { output(name: string, value: JsonValue) { stepOutputs[name] = value; }, + // deprecated in favor of templateInfo metadata: task.spec.metadata, + templateInfo: task.spec.templateInfo, }); // Remove all temporary directories that were created when executing the action diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 1597c575ac..1b635b695c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -22,7 +22,8 @@ import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { TaskContext, TaskSpec, TaskSecrets } from './types'; +import { TaskContext, TaskSecrets } from './types'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; const realFiles = Object.fromEntries( [ @@ -163,7 +164,7 @@ describe('DefaultWorkflowRunner', () => { }); it('should pass metadata through', async () => { - const templateName = 'template name'; + const entityRef = `template:default/templateName`; const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', parameters: {}, @@ -176,13 +177,13 @@ describe('DefaultWorkflowRunner', () => { input: { foo: 1 }, }, ], - metadata: { name: templateName }, + templateInfo: { entityRef }, }); await runner.execute(task); - expect(fakeActionHandler.mock.calls[0][0].metadata).toEqual({ - name: templateName, + expect(fakeActionHandler.mock.calls[0][0].templateInfo).toEqual({ + entityRef, }); }); @@ -203,13 +204,15 @@ describe('DefaultWorkflowRunner', () => { ], }, { - token: fakeToken, + backstageToken: fakeToken, }, ); await runner.execute(task); - expect(fakeActionHandler.mock.calls[0][0].token).toEqual(fakeToken); + expect(fakeActionHandler.mock.calls[0][0].secrets).toEqual( + expect.objectContaining({ backstageToken: fakeToken }), + ); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 889e94d000..fb9da56c22 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -15,14 +15,7 @@ */ import { ScmIntegrations } from '@backstage/integration'; -import { - TaskContext, - TaskSpec, - TaskSpecV1beta3, - TaskStep, - WorkflowResponse, - WorkflowRunner, -} from './types'; +import { TaskContext, WorkflowResponse, WorkflowRunner } from './types'; import * as winston from 'winston'; import fs from 'fs-extra'; import path from 'path'; @@ -39,6 +32,11 @@ import { SecureTemplater, SecureTemplateRenderer, } from '../../lib/templating/SecureTemplater'; +import { + TaskSpec, + TaskSpecV1beta3, + TaskStep, +} from '@backstage/plugin-scaffolder-common'; type NunjucksWorkflowRunnerOptions = { workingDirectory: string; @@ -258,17 +256,10 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const tmpDirs = new Array(); const stepOutput: { [outputName: string]: JsonValue } = {}; - if (!task.spec.metadata) { - console.warn( - 'DEPRECATION NOTICE: metadata is undefined. metadata will be required in the future.', - ); - } - await action.handler({ + // deprecated in favourof templateInfo.baseUrl baseUrl: task.spec.baseUrl, input, - // this token is deprecated, and will be removed in favour of secrets.backstageToken instead. - token: task.secrets?.token, secrets: task.secrets ?? {}, logger: taskLogger, logStream: streamLogger, @@ -283,7 +274,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { output(name: string, value: JsonValue) { stepOutput[name] = value; }, + // deprecated in favour of templateInfo metadata: task.spec.metadata, + templateInfo: task.spec.templateInfo, }); // Remove all temporary directories that were created when executing the action diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 130f6ced13..52fd175dc8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -16,9 +16,10 @@ import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker, TaskManager } from './StorageTaskBroker'; -import { TaskSecrets, TaskSpec, SerializedTaskEvent } from './types'; +import { TaskSecrets, SerializedTaskEvent } from './types'; async function createStore(): Promise { const manager = DatabaseManager.fromConfig( @@ -38,7 +39,7 @@ async function createStore(): Promise { describe('StorageTaskBroker', () => { let storage: DatabaseTaskStore; - const fakeSecrets = { token: 'secret' } as TaskSecrets; + const fakeSecrets = { backstageToken: 'secret' } as TaskSecrets; beforeAll(async () => { storage = await createStore(); @@ -47,7 +48,7 @@ describe('StorageTaskBroker', () => { const logger = getVoidLogger(); it('should claim a dispatched work item', async () => { const broker = new StorageTaskBroker(storage, logger); - await broker.dispatch({} as TaskSpec); + await broker.dispatch({ spec: {} as TaskSpec }); await expect(broker.claim()).resolves.toEqual(expect.any(TaskManager)); }); @@ -57,15 +58,15 @@ describe('StorageTaskBroker', () => { await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); - await broker.dispatch({} as TaskSpec); + await broker.dispatch({ spec: {} as TaskSpec }); await expect(promise).resolves.toEqual(expect.any(TaskManager)); }); it('should dispatch multiple items and claim them in order', async () => { const broker = new StorageTaskBroker(storage, logger); - await broker.dispatch({ steps: [{ id: 'a' }] } as TaskSpec); - await broker.dispatch({ steps: [{ id: 'b' }] } as TaskSpec); - await broker.dispatch({ steps: [{ id: 'c' }] } as TaskSpec); + await broker.dispatch({ spec: { steps: [{ id: 'a' }] } as TaskSpec }); + await broker.dispatch({ spec: { steps: [{ id: 'b' }] } as TaskSpec }); + await broker.dispatch({ spec: { steps: [{ id: 'c' }] } as TaskSpec }); const taskA = await broker.claim(); const taskB = await broker.claim(); @@ -80,14 +81,14 @@ describe('StorageTaskBroker', () => { it('should store secrets', async () => { const broker = new StorageTaskBroker(storage, logger); - await broker.dispatch({} as TaskSpec, fakeSecrets); + await broker.dispatch({ spec: {} as TaskSpec, secrets: fakeSecrets }); const task = await broker.claim(); expect(task.secrets).toEqual(fakeSecrets); }, 10000); it('should complete a task', async () => { const broker = new StorageTaskBroker(storage, logger); - const dispatchResult = await broker.dispatch({} as TaskSpec); + const dispatchResult = await broker.dispatch({ spec: {} as TaskSpec }); const task = await broker.claim(); await task.complete('completed'); const taskRow = await storage.getTask(dispatchResult.taskId); @@ -96,7 +97,10 @@ describe('StorageTaskBroker', () => { it('should remove secrets after picking up a task', async () => { const broker = new StorageTaskBroker(storage, logger); - const dispatchResult = await broker.dispatch({} as TaskSpec, fakeSecrets); + const dispatchResult = await broker.dispatch({ + spec: {} as TaskSpec, + secrets: fakeSecrets, + }); await broker.claim(); const taskRow = await storage.getTask(dispatchResult.taskId); @@ -105,7 +109,7 @@ describe('StorageTaskBroker', () => { it('should fail a task', async () => { const broker = new StorageTaskBroker(storage, logger); - const dispatchResult = await broker.dispatch({} as TaskSpec); + const dispatchResult = await broker.dispatch({ spec: {} as TaskSpec }); const task = await broker.claim(); await task.complete('failed'); const taskRow = await storage.getTask(dispatchResult.taskId); @@ -116,12 +120,12 @@ describe('StorageTaskBroker', () => { const broker1 = new StorageTaskBroker(storage, logger); const broker2 = new StorageTaskBroker(storage, logger); - const { taskId } = await broker1.dispatch({} as TaskSpec); + const { taskId } = await broker1.dispatch({ spec: {} as TaskSpec }); const logPromise = new Promise(resolve => { const observedEvents = new Array(); - broker2.observe({ taskId, after: undefined }, (_err, { events }) => { + broker2.event$({ taskId, after: undefined }).subscribe(({ events }) => { observedEvents.push(...events); if (events.some(e => e.type === 'completion')) { resolve(observedEvents); @@ -143,9 +147,11 @@ describe('StorageTaskBroker', () => { ]); const afterLogs = await new Promise(resolve => { - broker2.observe({ taskId, after: logs[1].id }, (_err, { events }) => - resolve(events.map(e => e.body.message as string)), - ); + broker2 + .event$({ taskId, after: logs[1].id }) + .subscribe(({ events }) => + resolve(events.map(e => e.body.message as string)), + ); }); expect(afterLogs).toEqual([ 'log 3', @@ -155,7 +161,7 @@ describe('StorageTaskBroker', () => { it('should heartbeat', async () => { const broker = new StorageTaskBroker(storage, logger); - const { taskId } = await broker.dispatch({} as TaskSpec); + const { taskId } = await broker.dispatch({ spec: {} as TaskSpec }); const task = await broker.claim(); const initialTask = await storage.getTask(taskId); @@ -173,7 +179,7 @@ describe('StorageTaskBroker', () => { it('should be update the status to failed if heartbeat fails', async () => { const broker = new StorageTaskBroker(storage, logger); - const { taskId } = await broker.dispatch({} as TaskSpec); + const { taskId } = await broker.dispatch({ spec: {} as TaskSpec }); const task = await broker.claim(); jest diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 7e7ebe5436..0fd593f2f3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JsonObject } from '@backstage/types'; -import { assertError } from '@backstage/errors'; +import { JsonObject, Observable } from '@backstage/types'; +import ObservableImpl from 'zen-observable'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { Logger } from 'winston'; import { - CompletedTaskState, + TaskCompletionState, TaskContext, TaskSecrets, - TaskSpec, TaskStore, TaskBroker, - DispatchResult, SerializedTaskEvent, SerializedTask, } from './types'; +import { TaskBrokerDispatchOptions } from '.'; /** * TaskManager @@ -38,48 +38,48 @@ export class TaskManager implements TaskContext { private heartbeatTimeoutId?: ReturnType; - static create(state: TaskState, storage: TaskStore, logger: Logger) { - const agent = new TaskManager(state, storage, logger); + static create(task: CurrentClaimedTask, storage: TaskStore, logger: Logger) { + const agent = new TaskManager(task, storage, logger); agent.startTimeout(); return agent; } // Runs heartbeat internally private constructor( - private readonly state: TaskState, + private readonly task: CurrentClaimedTask, private readonly storage: TaskStore, private readonly logger: Logger, ) {} get spec() { - return this.state.spec; + return this.task.spec; } get secrets() { - return this.state.secrets; + return this.task.secrets; } async getWorkspaceName() { - return this.state.taskId; + return this.task.taskId; } get done() { return this.isDone; } - async emitLog(message: string, metadata?: JsonObject): Promise { + async emitLog(message: string, logMetadata?: JsonObject): Promise { await this.storage.emitLogEvent({ - taskId: this.state.taskId, - body: { message, ...metadata }, + taskId: this.task.taskId, + body: { message, ...logMetadata }, }); } async complete( - result: CompletedTaskState, + result: TaskCompletionState, metadata?: JsonObject, ): Promise { await this.storage.completeTask({ - taskId: this.state.taskId, + taskId: this.task.taskId, status: result === 'failed' ? 'failed' : 'completed', eventBody: { message: `Run completed with status: ${result}`, @@ -95,13 +95,13 @@ export class TaskManager implements TaskContext { private startTimeout() { this.heartbeatTimeoutId = setTimeout(async () => { try { - await this.storage.heartbeatTask(this.state.taskId); + await this.storage.heartbeatTask(this.task.taskId); this.startTimeout(); } catch (error) { this.isDone = true; this.logger.error( - `Heartbeat for task ${this.state.taskId} failed`, + `Heartbeat for task ${this.task.taskId} failed`, error, ); } @@ -110,16 +110,24 @@ export class TaskManager implements TaskContext { } /** - * TaskState + * Stores the state of the current claimed task passed to the TaskContext * * @public */ -export interface TaskState { +export interface CurrentClaimedTask { spec: TaskSpec; taskId: string; secrets?: TaskSecrets; } +/** + * TaskState + * + * @public + * @deprecated use CurrentClaimedTask instead + */ +export type TaskState = CurrentClaimedTask; + function defer() { let resolve = () => {}; const promise = new Promise(_resolve => { @@ -155,10 +163,9 @@ export class StorageTaskBroker implements TaskBroker { } async dispatch( - spec: TaskSpec, - secrets?: TaskSecrets, - ): Promise { - const taskRow = await this.storage.createTask(spec, secrets); + options: TaskBrokerDispatchOptions, + ): Promise<{ taskId: string }> { + const taskRow = await this.storage.createTask(options); this.signalDispatch(); return { taskId: taskRow.taskId, @@ -169,47 +176,37 @@ export class StorageTaskBroker implements TaskBroker { return this.storage.getTask(taskId); } - observe( - options: { - taskId: string; - after: number | undefined; - }, - callback: ( - error: Error | undefined, - result: { events: SerializedTaskEvent[] }, - ) => void, - ): { unsubscribe: () => void } { - const { taskId } = options; + event$(options: { + taskId: string; + after?: number; + }): Observable<{ events: SerializedTaskEvent[] }> { + return new ObservableImpl(observer => { + const { taskId } = options; - let cancelled = false; - const unsubscribe = () => { - cancelled = true; - }; - - (async () => { let after = options.after; - while (!cancelled) { - const result = await this.storage.listEvents({ taskId, after: after }); - const { events } = result; - if (events.length) { - after = events[events.length - 1].id; - try { - callback(undefined, result); - } catch (error) { - assertError(error); - callback(error, { events: [] }); + let cancelled = false; + + (async () => { + while (!cancelled) { + const result = await this.storage.listEvents({ taskId, after }); + const { events } = result; + if (events.length) { + after = events[events.length - 1].id; + observer.next(result); } + + await new Promise(resolve => setTimeout(resolve, 1000)); } + })(); - await new Promise(resolve => setTimeout(resolve, 1000)); - } - })(); - - return { unsubscribe }; + return () => { + cancelled = true; + }; + }); } - async vacuumTasks(timeoutS: { timeoutS: number }): Promise { - const { tasks } = await this.storage.listStaleTasks(timeoutS); + async vacuumTasks(options: { timeoutS: number }): Promise { + const { tasks } = await this.storage.listStaleTasks(options); await Promise.all( tasks.map(async task => { try { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 9b6baf3b84..9ff6b9c8a1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -91,12 +91,14 @@ describe('TaskWorker', () => { }); await broker.dispatch({ - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', + spec: { + apiVersion: 'backstage.io/v1beta2', + steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], + output: { + result: '{{ steps.test.output.testOutput }}', + }, + values: {}, }, - values: {}, }); const task = await broker.claim(); @@ -122,12 +124,14 @@ describe('TaskWorker', () => { }); await broker.dispatch({ - apiVersion: 'scaffolder.backstage.io/v1beta3', - steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', + spec: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], + output: { + result: '{{ steps.test.output.testOutput }}', + }, + parameters: {}, }, - parameters: {}, }); const task = await broker.claim(); @@ -151,12 +155,14 @@ describe('TaskWorker', () => { }); const { taskId } = await broker.dispatch({ - apiVersion: 'scaffolder.backstage.io/v1beta3', - steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', + spec: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], + output: { + result: '{{ steps.test.output.testOutput }}', + }, + parameters: {}, }, - parameters: {}, }); const task = await broker.claim(); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index f563a08df6..b108a3308d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -14,25 +14,28 @@ * limitations under the License. */ export { DatabaseTaskStore } from './DatabaseTaskStore'; +export type { DatabaseTaskStoreOptions } from './DatabaseTaskStore'; export { TaskManager } from './StorageTaskBroker'; -export type { TaskState } from './StorageTaskBroker'; +export type { CurrentClaimedTask, TaskState } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; export type { CreateWorkerOptions } from './TaskWorker'; export type { TaskSecrets, - TaskSpec, + TaskCompletionState, CompletedTaskState, TaskStoreEmitOptions, TaskStoreListEventsOptions, SerializedTask, SerializedTaskEvent, - TaskSpecV1beta2, - TaskSpecV1beta3, Status, + TaskStatus, TaskEventType, TaskBroker, TaskContext, TaskStore, DispatchResult, - TemplateMetadata, + TaskBrokerDispatchResult, + TaskBrokerDispatchOptions, + TaskStoreCreateTaskOptions, + TaskStoreCreateTaskResult, } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 8caee87f57..57b92e0222 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,29 +14,15 @@ * limitations under the License. */ -import { JsonValue, JsonObject } from '@backstage/types'; -import { - TaskSpec, - TaskStep, - TemplateMetadata, - TaskSpecV1beta2, - TaskSpecV1beta3, -} from '@backstage/plugin-scaffolder-common'; - -export type { - TaskSpec, - TaskStep, - TemplateMetadata, - TaskSpecV1beta2, - TaskSpecV1beta3, -}; +import { JsonValue, JsonObject, Observable } from '@backstage/types'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; /** - * Status + * The status of each step of the Task * * @public */ -export type Status = +export type TaskStatus = | 'open' | 'processing' | 'failed' @@ -44,11 +30,27 @@ export type Status = | 'completed'; /** - * CompletedTaskState + * The status of each step of the Task + * + * @public + * @deprecated use TaskStatus instead + */ +export type Status = TaskStatus; + +/** + * The state of a completed task. * * @public */ -export type CompletedTaskState = 'failed' | 'completed'; +export type TaskCompletionState = 'failed' | 'completed'; + +/** + * The state of a completed task. + * + * @public + * @deprecated use TaskCompletionState instead + */ +export type CompletedTaskState = TaskCompletionState; /** * SerializedTask @@ -58,7 +60,7 @@ export type CompletedTaskState = 'failed' | 'completed'; export type SerializedTask = { id: string; spec: TaskSpec; - status: Status; + status: TaskStatus; createdAt: string; lastHeartbeatAt?: string; secrets?: TaskSecrets; @@ -90,19 +92,36 @@ export type SerializedTaskEvent = { * @public */ export type TaskSecrets = Record & { - /** @deprecated Use `backstageToken` instead */ - token?: string; backstageToken?: string; }; +/** + * The result of {@link TaskBroker.dispatch} + * + * @public + */ +export type TaskBrokerDispatchResult = { + taskId: string; +}; + +/** + * The options passed to {@link TaskBroker.dispatch} + * Currently a spec and optional secrets + * + * @public + */ +export type TaskBrokerDispatchOptions = { + spec: TaskSpec; + secrets?: TaskSecrets; +}; + /** * DispatchResult * * @public + * @deprecated use TaskBrokerDispatchResult instead */ -export type DispatchResult = { - taskId: string; -}; +export type DispatchResult = TaskBrokerDispatchResult; /** * Task @@ -113,8 +132,8 @@ export interface TaskContext { spec: TaskSpec; secrets?: TaskSecrets; done: boolean; - emitLog(message: string, metadata?: JsonValue): Promise; - complete(result: CompletedTaskState, metadata?: JsonValue): Promise; + emitLog(message: string, logMetadata?: JsonObject): Promise; + complete(result: TaskCompletionState, metadata?: JsonObject): Promise; getWorkspaceName(): Promise; } @@ -125,18 +144,14 @@ export interface TaskContext { */ export interface TaskBroker { claim(): Promise; - dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; - vacuumTasks(timeoutS: { timeoutS: number }): Promise; - observe( - options: { - taskId: string; - after: number | undefined; - }, - callback: ( - error: Error | undefined, - result: { events: SerializedTaskEvent[] }, - ) => void, - ): { unsubscribe: () => void }; + dispatch( + options: TaskBrokerDispatchOptions, + ): Promise; + vacuumTasks(options: { timeoutS: number }): Promise; + event$(options: { + taskId: string; + after: number | undefined; + }): Observable<{ events: SerializedTaskEvent[] }>; get(taskId: string): Promise; } @@ -145,9 +160,9 @@ export interface TaskBroker { * * @public */ -export type TaskStoreEmitOptions = { +export type TaskStoreEmitOptions = { taskId: string; - body: JsonObject; + body: TBody; }; /** @@ -160,6 +175,23 @@ export type TaskStoreListEventsOptions = { after?: number | undefined; }; +/** + * The options passed to {@link TaskStore.createTask} + * @public + */ +export type TaskStoreCreateTaskOptions = { + spec: TaskSpec; + secrets?: TaskSecrets; +}; + +/** + * The response from {@link TaskStore.createTask} + * @public + */ +export type TaskStoreCreateTaskResult = { + taskId: string; +}; + /** * TaskStore * @@ -167,14 +199,13 @@ export type TaskStoreListEventsOptions = { */ export interface TaskStore { createTask( - task: TaskSpec, - secrets?: TaskSecrets, - ): Promise<{ taskId: string }>; + options: TaskStoreCreateTaskOptions, + ): Promise; getTask(taskId: string): Promise; claimTask(): Promise; completeTask(options: { taskId: string; - status: Status; + status: TaskStatus; eventBody: JsonObject; }): Promise; heartbeatTask(taskId: string): Promise; diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index 6741ab3379..e49c28cb74 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -14,14 +14,21 @@ * limitations under the License. */ +import { CatalogApi } from '@backstage/catalog-client'; import { Entity, - LOCATION_ANNOTATION, - parseLocationReference, - SOURCE_LOCATION_ANNOTATION, + ANNOTATION_LOCATION, + parseLocationRef, + ANNOTATION_SOURCE_LOCATION, + EntityName, + DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { assertError } from '@backstage/errors'; +import { assertError, InputError, NotFoundError } from '@backstage/errors'; +import { + TemplateEntityV1beta2, + TemplateEntityV1beta3, +} from '@backstage/plugin-scaffolder-common'; import fs from 'fs-extra'; import os from 'os'; import { Logger } from 'winston'; @@ -59,15 +66,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') { @@ -78,3 +85,31 @@ export function getEntityBaseUrl(entity: Entity): string | undefined { // what the url is pointing to makes sense to use as a baseUrl return undefined; } + +/** + * Will use the provided CatalogApi to go find the given template entity with an additional token. + * Returns the matching template, or throws a NotFoundError if no such template existed. + */ +export async function findTemplate(options: { + entityRef: EntityName; + token?: string; + catalogApi: CatalogApi; +}): Promise { + const { entityRef, token, catalogApi } = options; + + if (entityRef.namespace.toLocaleLowerCase('en-US') !== DEFAULT_NAMESPACE) { + throw new InputError( + `Invalid namespace, only '${DEFAULT_NAMESPACE}' namespace is supported`, + ); + } + if (entityRef.kind.toLocaleLowerCase('en-US') !== 'template') { + throw new InputError(`Invalid kind, only 'Template' kind is supported`); + } + + const template = await catalogApi.getEntityByName(entityRef, { token }); + if (!template) { + throw new NotFoundError(`Template ${entityRef} not found`); + } + + return template as TemplateEntityV1beta3 | TemplateEntityV1beta2; +} diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index d8ba15279f..fa1b649231 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -36,8 +36,9 @@ import { UrlReaders, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; -import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; import { ConfigReader } from '@backstage/config'; +import ObservableImpl from 'zen-observable'; import express from 'express'; import request from 'supertest'; /** @@ -48,11 +49,12 @@ import request from 'supertest'; */ import { createRouter, DatabaseTaskStore, TaskBroker } from '../index'; import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; +import { stringifyEntityRef } from '@backstage/catalog-model'; -const createCatalogClient = (templates: any[] = []) => +const createCatalogClient = (template: any) => ({ - getEntities: async () => ({ items: templates }), - } as CatalogApi); + getEntityByName: async () => template, + } as unknown as CatalogApi); function createDatabase(): PluginDatabaseManager { return DatabaseManager.fromConfig( @@ -113,13 +115,13 @@ describe('createRouter', () => { jest.spyOn(taskBroker, 'dispatch'); jest.spyOn(taskBroker, 'get'); - jest.spyOn(taskBroker, 'observe'); + jest.spyOn(taskBroker, 'event$'); const router = await createRouter({ logger: getVoidLogger(), config: new ConfigReader({}), database: createDatabase(), - catalogClient: createCatalogClient([template]), + catalogClient: createCatalogClient(template), containerRunner: new DockerContainerRunner({} as any), reader: mockUrlReader, taskBroker, @@ -145,7 +147,10 @@ describe('createRouter', () => { const response = await request(app) .post('/v2/tasks') .send({ - templateName: 'create-react-app-template', + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), values: { storePath: 'https://github.com/backstage/backstage', }, @@ -164,7 +169,10 @@ describe('createRouter', () => { const response = await request(app) .post('/v2/tasks') .send({ - templateName: 'create-react-app-template', + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), values: { required: 'required-value', }, @@ -182,7 +190,7 @@ describe('createRouter', () => { spec: {} as any, status: 'completed', createdAt: '', - secrets: { token: 'secret' }, + secrets: { backstageToken: 'secret' }, }); const response = await request(app).get(`/v2/tasks/a-random-id`); @@ -194,37 +202,38 @@ describe('createRouter', () => { describe('GET /v2/tasks/:taskId/eventstream', () => { it('should return log messages', async () => { - const unsubscribe = jest.fn(); + let subscriber: ZenObservable.SubscriptionObserver; ( - taskBroker.observe as jest.Mocked['observe'] - ).mockImplementation(({ taskId }, callback) => { - // emit after this function returned - setImmediate(() => { - callback(undefined, { - events: [ - { - id: 0, - taskId, - type: 'log', - createdAt: '', - body: { message: 'My log message' }, - }, - ], - }); - callback(undefined, { - events: [ - { - id: 1, - taskId, - type: 'completion', - createdAt: '', - body: { message: 'Finished!' }, - }, - ], + taskBroker.event$ as jest.Mocked['event$'] + ).mockImplementation(({ taskId }) => { + return new ObservableImpl(observer => { + subscriber = observer; + setImmediate(() => { + observer.next({ + events: [ + { + id: 0, + taskId, + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }, + ], + }); + observer.next({ + events: [ + { + id: 1, + taskId, + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }, + ], + }); }); }); - - return { unsubscribe }; + // emit after this function returned }); let statusCode: any = undefined; @@ -264,34 +273,32 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ `); - expect(taskBroker.observe).toBeCalledTimes(1); - expect(taskBroker.observe).toBeCalledWith( - { taskId: 'a-random-id' }, - expect.any(Function), - ); - - expect(unsubscribe).toBeCalledTimes(1); + expect(taskBroker.event$).toBeCalledTimes(1); + expect(taskBroker.event$).toBeCalledWith({ taskId: 'a-random-id' }); + expect(subscriber!.closed).toBe(true); }); it('should return log messages with after query', async () => { - const unsubscribe = jest.fn(); + let subscriber: ZenObservable.SubscriptionObserver; ( - taskBroker.observe as jest.Mocked['observe'] - ).mockImplementation(({ taskId }, callback) => { - setImmediate(() => { - callback(undefined, { - events: [ - { - id: 1, - taskId, - type: 'completion', - createdAt: '', - body: { message: 'Finished!' }, - }, - ], + taskBroker.event$ as jest.Mocked['event$'] + ).mockImplementation(({ taskId }) => { + return new ObservableImpl(observer => { + subscriber = observer; + setImmediate(() => { + observer.next({ + events: [ + { + id: 1, + taskId, + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }, + ], + }); }); }); - return { unsubscribe }; }); let statusCode: any = undefined; @@ -318,41 +325,43 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect(statusCode).toBe(200); expect(headers['content-type']).toBe('text/event-stream'); - expect(taskBroker.observe).toBeCalledTimes(1); - expect(taskBroker.observe).toBeCalledWith( - { taskId: 'a-random-id', after: 10 }, - expect.any(Function), - ); + expect(taskBroker.event$).toBeCalledTimes(1); + expect(taskBroker.event$).toBeCalledWith({ + taskId: 'a-random-id', + after: 10, + }); - expect(unsubscribe).toBeCalledTimes(1); + expect(subscriber!.closed).toBe(true); }); }); describe('GET /v2/tasks/:taskId/events', () => { it('should return log messages', async () => { - const unsubscribe = jest.fn(); + let subscriber: ZenObservable.SubscriptionObserver; ( - taskBroker.observe as jest.Mocked['observe'] - ).mockImplementation(({ taskId }, callback) => { - callback(undefined, { - events: [ - { - id: 0, - taskId, - type: 'log', - createdAt: '', - body: { message: 'My log message' }, - }, - { - id: 1, - taskId, - type: 'completion', - createdAt: '', - body: { message: 'Finished!' }, - }, - ], + taskBroker.event$ as jest.Mocked['event$'] + ).mockImplementation(({ taskId }) => { + return new ObservableImpl(observer => { + subscriber = observer; + observer.next({ + events: [ + { + id: 0, + taskId, + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }, + { + id: 1, + taskId, + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }, + ], + }); }); - return { unsubscribe }; }); const response = await request(app).get('/v2/tasks/a-random-id/events'); @@ -375,21 +384,20 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ }, ]); - expect(taskBroker.observe).toBeCalledTimes(1); - expect(taskBroker.observe).toBeCalledWith( - { taskId: 'a-random-id' }, - expect.any(Function), - ); - expect(unsubscribe).toBeCalledTimes(1); + expect(taskBroker.event$).toBeCalledTimes(1); + expect(taskBroker.event$).toBeCalledWith({ taskId: 'a-random-id' }); + expect(subscriber!.closed).toBe(true); }); it('should return log messages with after query', async () => { - const unsubscribe = jest.fn(); + let subscriber: ZenObservable.SubscriptionObserver; ( - taskBroker.observe as jest.Mocked['observe'] - ).mockImplementation((_, callback) => { - callback(undefined, { events: [] }); - return { unsubscribe }; + taskBroker.event$ as jest.Mocked['event$'] + ).mockImplementation(() => { + return new ObservableImpl(observer => { + subscriber = observer; + observer.next({ events: [] }); + }); }); const response = await request(app) @@ -399,12 +407,12 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect(response.status).toEqual(200); expect(response.body).toEqual([]); - expect(taskBroker.observe).toBeCalledTimes(1); - expect(taskBroker.observe).toBeCalledWith( - { taskId: 'a-random-id', after: 10 }, - expect.any(Function), - ); - expect(unsubscribe).toBeCalledTimes(1); + expect(taskBroker.event$).toBeCalledTimes(1); + expect(taskBroker.event$).toBeCalledWith({ + taskId: 'a-random-id', + after: 10, + }); + expect(subscriber!.closed).toBe(true); }); }); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 85ed8a60ab..8f793a34d8 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -20,27 +20,33 @@ import { UrlReader, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; -import { Entity, TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { InputError, NotFoundError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { + TemplateEntityV1beta2, + TemplateEntityV1beta3, + TaskSpecV1beta3, + TaskSpec, + TaskSpecV1beta2, +} from '@backstage/plugin-scaffolder-common'; import express from 'express'; import Router from 'express-promise-router'; import { validate } from 'jsonschema'; import { Logger } from 'winston'; -import { CatalogEntityClient, TemplateFilter } from '../lib'; +import { TemplateFilter } from '../lib'; import { createBuiltinActions, DatabaseTaskStore, TaskBroker, - TaskSpec, TaskWorker, TemplateAction, TemplateActionRegistry, } from '../scaffolder'; import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; -import { getEntityBaseUrl, getWorkingDirectory } from './helpers'; +import { getEntityBaseUrl, getWorkingDirectory, findTemplate } from './helpers'; /** * RouterOptions @@ -69,6 +75,7 @@ function isSupportedTemplate( ); } +/** @public */ export async function createRouter( options: RouterOptions, ): Promise { @@ -89,7 +96,6 @@ export async function createRouter( const logger = parentLogger.child({ plugin: 'scaffolder' }); const workingDirectory = await getWorkingDirectory(config, logger); - const entityClient = new CatalogEntityClient(catalogClient); const integrations = ScmIntegrations.fromConfig(config); let taskBroker: TaskBroker; @@ -136,19 +142,9 @@ export async function createRouter( '/v2/templates/:namespace/:kind/:name/parameter-schema', async (req, res) => { const { namespace, kind, name } = req.params; - - if (namespace !== 'default') { - throw new InputError( - `Invalid namespace, only 'default' namespace is supported`, - ); - } - if (kind.toLowerCase() !== 'template') { - throw new InputError( - `Invalid kind, only 'Template' kind is supported`, - ); - } - - const template = await entityClient.findTemplate(name, { + const template = await findTemplate({ + catalogApi: catalogClient, + entityRef: { kind, namespace, name }, token: getBearerToken(req.headers.authorization), }); if (isSupportedTemplate(template)) { @@ -180,19 +176,33 @@ export async function createRouter( res.json(actionsList); }) .post('/v2/tasks', async (req, res) => { - const templateName: string = req.body.templateName; + const templateRef: string = req.body.templateRef; + const { kind, namespace, name } = parseEntityRef(templateRef, { + defaultKind: 'template', + }); const values = req.body.values; const token = getBearerToken(req.headers.authorization); - const template = await entityClient.findTemplate(templateName, { - token, + const template = await findTemplate({ + catalogApi: catalogClient, + entityRef: { kind, namespace, name }, + token: getBearerToken(req.headers.authorization), }); let taskSpec: TaskSpec; if (isSupportedTemplate(template)) { + if (template.apiVersion === 'backstage.io/v1beta2') { + logger.warn( + `Scaffolding ${stringifyEntityRef( + template, + )} with deprecated apiVersion ${ + template.apiVersion + }. Please migrate the template to backstage.io/v1beta3. https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3`, + ); + } + for (const parameters of [template.spec.parameters ?? []].flat()) { const result = validate(values, parameters); - if (!result.valid) { res.status(400).json({ errors: result.errors }); return; @@ -201,32 +211,40 @@ export async function createRouter( const baseUrl = getEntityBaseUrl(template); + const baseTaskSpec = { + baseUrl, + steps: template.spec.steps.map((step, index) => ({ + ...step, + id: step.id ?? `step-${index + 1}`, + name: step.name ?? step.action, + })), + output: template.spec.output ?? {}, + + // deprecated in favour of templateInfo + metadata: { name: template.metadata?.name }, + + templateInfo: { + entityRef: stringifyEntityRef({ + kind, + namespace, + name: template.metadata?.name, + }), + baseUrl, + }, + }; + taskSpec = template.apiVersion === 'backstage.io/v1beta2' - ? { + ? ({ + ...baseTaskSpec, apiVersion: template.apiVersion, - baseUrl, values, - steps: template.spec.steps.map((step, index) => ({ - ...step, - id: step.id ?? `step-${index + 1}`, - name: step.name ?? step.action, - })), - output: template.spec.output ?? {}, - metadata: { name: template.metadata?.name }, - } - : { + } as TaskSpecV1beta2) + : ({ + ...baseTaskSpec, apiVersion: template.apiVersion, - baseUrl, parameters: values, - steps: template.spec.steps.map((step, index) => ({ - ...step, - id: step.id ?? `step-${index + 1}`, - name: step.name ?? step.action, - })), - output: template.spec.output ?? {}, - metadata: { name: template.metadata?.name }, - }; + } as TaskSpecV1beta3); } else { throw new InputError( `Unsupported apiVersion field in schema entity, ${ @@ -235,11 +253,12 @@ export async function createRouter( ); } - const result = await taskBroker.dispatch(taskSpec, { - ...req.body.secrets, - backstageToken: token, - // This is deprecated, but we need to support it for now if people are running their own task broker. - token: token, + const result = await taskBroker.dispatch({ + spec: taskSpec, + secrets: { + ...req.body.secrets, + backstageToken: token, + }, }); res.status(201).json({ id: result.taskId }); @@ -269,15 +288,13 @@ export async function createRouter( }); // After client opens connection send all events as string - const { unsubscribe } = taskBroker.observe( - { taskId, after }, - (error, { events }) => { - if (error) { - logger.error( - `Received error from event stream when observing taskId '${taskId}', ${error}`, - ); - } - + const subscription = taskBroker.event$({ taskId, after }).subscribe({ + error: error => { + logger.error( + `Received error from event stream when observing taskId '${taskId}', ${error}`, + ); + }, + next: ({ events }) => { let shouldUnsubscribe = false; for (const event of events) { res.write( @@ -285,19 +302,18 @@ export async function createRouter( ); if (event.type === 'completion') { shouldUnsubscribe = true; - // Closing the event stream here would cause the frontend - // to automatically reconnect because it lost connection. } } // res.flush() is only available with the compression middleware res.flush?.(); - if (shouldUnsubscribe) unsubscribe(); + if (shouldUnsubscribe) subscription.unsubscribe(); }, - ); + }); + // When client closes connection we update the clients list // avoiding the disconnected one req.on('close', () => { - unsubscribe(); + subscription.unsubscribe(); logger.debug(`Event stream observing taskId '${taskId}' closed`); }); }) @@ -305,36 +321,29 @@ export async function createRouter( const { taskId } = req.params; const after = Number(req.query.after) || undefined; - let unsubscribe = () => {}; - // cancel the request after 30 seconds. this aligns with the recommendations of RFC 6202. const timeout = setTimeout(() => { - unsubscribe(); res.json([]); }, 30_000); // Get all known events after an id (always includes the completion event) and return the first callback - ({ unsubscribe } = taskBroker.observe( - { taskId, after }, - (error, { events }) => { - // stop the timeout + const subscription = taskBroker.event$({ taskId, after }).subscribe({ + error: error => { + logger.error( + `Received error from event stream when observing taskId '${taskId}', ${error}`, + ); + }, + next: ({ events }) => { clearTimeout(timeout); - unsubscribe(); - - if (error) { - logger.error( - `Received error from log when observing taskId '${taskId}', ${error}`, - ); - } - + subscription.unsubscribe(); res.json(events); }, - )); + }); // When client closes connection we update the clients list // avoiding the disconnected one req.on('close', () => { - unsubscribe(); + subscription.unsubscribe(); clearTimeout(timeout); }); }); diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index fb161f6a4a..db702973fe 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-scaffolder-common +## 0.2.2 + +### Patch Changes + +- a4d53fe18e: **DEPRECATED** - The `TaskSpec.metadata` and `TaskSpec.baseUrl` has been deprecated in favour of the new `TaskSpec.templateInfo`. + The `baseUrl` is now found on the `templateInfo` object, and the name can be inferred from the `templateInfo.entityRef` property. + + Usages of `TaskSpec.metadata.name` or `ctx.metadata.name` in Actions should migrate to using `parseEntityRef(taskSpec.templateInfo.entityRef)` to get the parsed entity triplet. + + Usages of `ctx.baseUrl` in Actions should migrate to using `ctx.templateInfo.baseUrl` instead. + +- Updated dependencies + - @backstage/catalog-model@0.11.0 + +## 0.2.1 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/catalog-model@0.10.1 + - @backstage/types@0.1.3 + +## 0.2.0 + +### Minor Changes + +- 5e585bbc7f: **BREAKING**: Removed the `templateEntityV1beta3Schema` export + +### Patch Changes + +- e72d371296: Added `TemplateEntityV1beta2` which was moved here from + `@backstage/plugin-scaffolder-common`. It has also been marked as deprecated in + the process - please consider [migrating to `v1beta3` + templates](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3). +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/catalog-model@0.10.0 + - @backstage/types@0.1.2 + ## 0.1.3 ### Patch Changes diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 3bca8e74ed..1872aa3ed8 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -5,19 +5,19 @@ ```ts import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; -import { JSONSchema } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; +import { KindValidator } from '@backstage/catalog-model'; // @public export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; -// @public +// @public @deprecated export interface TaskSpecV1beta2 { // (undocumented) apiVersion: 'backstage.io/v1beta2'; - // (undocumented) + // @deprecated (undocumented) baseUrl?: string; - // (undocumented) + // @deprecated (undocumented) metadata?: TemplateMetadata; // (undocumented) output: { @@ -26,6 +26,8 @@ export interface TaskSpecV1beta2 { // (undocumented) steps: TaskStep[]; // (undocumented) + templateInfo?: TemplateInfo; + // (undocumented) values: JsonObject; } @@ -33,9 +35,9 @@ export interface TaskSpecV1beta2 { export interface TaskSpecV1beta3 { // (undocumented) apiVersion: 'scaffolder.backstage.io/v1beta3'; - // (undocumented) + // @deprecated (undocumented) baseUrl?: string; - // (undocumented) + // @deprecated (undocumented) metadata?: TemplateMetadata; // (undocumented) output: { @@ -45,6 +47,8 @@ export interface TaskSpecV1beta3 { parameters: JsonObject; // (undocumented) steps: TaskStep[]; + // (undocumented) + templateInfo?: TemplateInfo; } // @public @@ -61,7 +65,34 @@ export interface TaskStep { name: string; } -// @public (undocumented) +// @public @deprecated +export interface TemplateEntityV1beta2 extends Entity { + // (undocumented) + apiVersion: 'backstage.io/v1beta2'; + // (undocumented) + kind: 'Template'; + // (undocumented) + spec: { + type: string; + parameters?: JsonObject | JsonObject[]; + steps: Array<{ + id?: string; + name?: string; + action: string; + input?: JsonObject; + if?: string | boolean; + }>; + output?: { + [name: string]: string; + }; + owner?: string; + }; +} + +// @public @deprecated +export const templateEntityV1beta2Validator: KindValidator; + +// @public export interface TemplateEntityV1beta3 extends Entity { // (undocumented) apiVersion: 'scaffolder.backstage.io/v1beta3'; @@ -85,10 +116,16 @@ export interface TemplateEntityV1beta3 extends Entity { }; } -// @public (undocumented) -export const templateEntityV1beta3Schema: JSONSchema; +// @public +export const templateEntityV1beta3Validator: KindValidator; // @public +export type TemplateInfo = { + entityRef: string; + baseUrl?: string; +}; + +// @public @deprecated export type TemplateMetadata = { name: string; }; diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 6b03a7c884..be48edd743 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "0.1.3", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -12,6 +12,9 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -25,21 +28,21 @@ "dist" ], "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "bugs": { "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/types": "^0.1.1" + "@backstage/catalog-model": "^0.11.0", + "@backstage/types": "^0.1.3" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0" + "@backstage/cli": "^0.14.1" } } diff --git a/plugins/scaffolder-common/src/TaskSpec.ts b/plugins/scaffolder-common/src/TaskSpec.ts index 8d8ccedbad..2c7eb26d5b 100644 --- a/plugins/scaffolder-common/src/TaskSpec.ts +++ b/plugins/scaffolder-common/src/TaskSpec.ts @@ -17,16 +17,29 @@ import { JsonValue, JsonObject } from '@backstage/types'; /** - * TemplateMetadata + * Metadata about the Template that was the originator of a scaffolder task, as + * stored in the database. * * @public + * @deprecated use templateInfo on the spec instead */ export type TemplateMetadata = { name: string; }; /** - * TaskStep + * Information about a template that is stored on a task specification. + * Includes a stringified entityRef, and the baseUrl which is usually the relative path of the template definition + * + * @public + */ +export type TemplateInfo = { + entityRef: string; + baseUrl?: string; +}; + +/** + * An individual step of a scaffolder task, as stored in the database. * * @public */ @@ -39,35 +52,45 @@ export interface TaskStep { } /** - * TaskSpecV1beta2 + * A scaffolder task as stored in the database, generated from a v1beta2 + * apiVersion Template. * * @public + * @deprecated Please convert your templates to TaskSpecV1beta3 on apiVersion + * scaffolder.backstage.io/v1beta3 */ export interface TaskSpecV1beta2 { apiVersion: 'backstage.io/v1beta2'; + /** @deprecated use templateInfo.baseUrl instead */ baseUrl?: string; values: JsonObject; steps: TaskStep[]; output: { [name: string]: string }; + /** @deprecated use templateInfo instead */ metadata?: TemplateMetadata; + templateInfo?: TemplateInfo; } /** - * TaskSpecV1beta3 + * A scaffolder task as stored in the database, generated from a v1beta3 + * apiVersion Template. * * @public */ export interface TaskSpecV1beta3 { apiVersion: 'scaffolder.backstage.io/v1beta3'; + /** @deprecated use templateInfo.baseUrl instead */ baseUrl?: string; parameters: JsonObject; steps: TaskStep[]; output: { [name: string]: JsonValue }; + /** @deprecated use templateInfo instead */ metadata?: TemplateMetadata; + templateInfo?: TemplateInfo; } /** - * TaskSpec + * A scaffolder task as stored in the database, generated from a Template. * * @public */ diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/plugins/scaffolder-common/src/Template.v1beta2.schema.json similarity index 100% rename from packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json rename to plugins/scaffolder-common/src/Template.v1beta2.schema.json diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta2.test.ts similarity index 100% rename from packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts rename to plugins/scaffolder-common/src/TemplateEntityV1beta2.test.ts diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta2.ts similarity index 58% rename from packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts rename to plugins/scaffolder-common/src/TemplateEntityV1beta2.ts index f4c2dfb5bc..280caec52f 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta2.ts @@ -14,15 +14,21 @@ * limitations under the License. */ +import { + Entity, + entityKindSchemaValidator, + KindValidator, +} from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; -import type { Entity } from '../entity/Entity'; -import schema from '../schema/kinds/Template.v1beta2.schema.json'; -import { ajvCompiledJsonSchemaValidator } from './util'; +import schema from './Template.v1beta2.schema.json'; /** - * Backstage catalog Template kind Entity. Templates are used by the Scaffolder plugin to create new Components. + * Backstage catalog Template kind Entity. Templates are used by the Scaffolder + * plugin to create new entities, such as Components. * * @public + * @deprecated Please convert your templates to TemplateEntityV1beta3 on + * apiVersion scaffolder.backstage.io/v1beta3 */ export interface TemplateEntityV1beta2 extends Entity { apiVersion: 'backstage.io/v1beta2'; @@ -42,10 +48,18 @@ export interface TemplateEntityV1beta2 extends Entity { }; } +const validator = entityKindSchemaValidator(schema); + /** - * {@link KindValidator} for {@link TemplateEntityV1beta2}. + * Entity data validator for {@link TemplateEntityV1beta2}. * * @public + * @deprecated Please convert your templates to TemplateEntityV1beta3 on + * apiVersion scaffolder.backstage.io/v1beta3 */ -export const templateEntityV1beta2Validator = - ajvCompiledJsonSchemaValidator(schema); +export const templateEntityV1beta2Validator: KindValidator = { + // TODO(freben): Emulate the old KindValidator until we fix that type + async check(data: Entity) { + return validator(data) === data; + }, +}; diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts index 2aefa5fd6b..13b699de69 100644 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts @@ -14,10 +14,20 @@ * limitations under the License. */ +import { + Entity, + entityKindSchemaValidator, + KindValidator, +} from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; -import { Entity } from '@backstage/catalog-model'; +import schema from './Template.v1beta3.schema.json'; -/** @public */ +/** + * Backstage catalog Template kind Entity. Templates are used by the Scaffolder + * plugin to create new entities, such as Components. + * + * @public + */ export interface TemplateEntityV1beta3 extends Entity { apiVersion: 'scaffolder.backstage.io/v1beta3'; kind: 'Template'; @@ -35,3 +45,17 @@ export interface TemplateEntityV1beta3 extends Entity { owner?: string; }; } + +const validator = entityKindSchemaValidator(schema); + +/** + * Entity data validator for {@link TemplateEntityV1beta3}. + * + * @public + */ +export const templateEntityV1beta3Validator: KindValidator = { + // TODO(freben): Emulate the old KindValidator until we fix that type + async check(data: Entity) { + return validator(data) === data; + }, +}; diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts index 1197f48fed..e74a5f988f 100644 --- a/plugins/scaffolder-common/src/index.ts +++ b/plugins/scaffolder-common/src/index.ts @@ -20,15 +20,8 @@ * @packageDocumentation */ -import { JSONSchema } from '@backstage/catalog-model'; -import v1beta3Schema from './Template.v1beta3.schema.json'; - -export type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3'; - -/** @public */ -export const templateEntityV1beta3Schema: JSONSchema = v1beta3Schema as Omit< - JSONSchema, - 'examples' ->; - export * from './TaskSpec'; +export { templateEntityV1beta2Validator } from './TemplateEntityV1beta2'; +export type { TemplateEntityV1beta2 } from './TemplateEntityV1beta2'; +export { templateEntityV1beta3Validator } from './TemplateEntityV1beta3'; +export type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3'; diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index dd7674a21b..1d1cddfa31 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,101 @@ # @backstage/plugin-scaffolder +## 0.13.0 + +### Minor Changes + +- 50e0242ac2: - **BREAKING** - `scaffolderApi.scaffold()` now takes one `options` argument instead of 3, the existing arguments should just be wrapped up in one object instead. + - **BREAKING** - `scaffolderApi.scaffold()` now returns an object instead of a single string for the job ID. It's now `{ taskId: string }` + - **BREAKING** - `scaffolderApi.scaffold()` now takes a `templateRef` instead of `templateName` as an argument in the options. This should be a valid stringified `entityRef`. + - **BREAKING** - `scaffolderApi.getIntegrationsList` now returns an object `{ integrations: { type: string, title: string, host: string }[] }` instead of just an array. +- a2589000ee: - **BREAKING** - Removed the `plugin` export, use `scaffolderPlugin` instead. + - **BREAKING** - Removed the `TextValuePicker` component export, you can inline this component instead as it's a simple wrapper around a `TextField` from `@material-ui/core`. + +### Patch Changes + +- 67a7c02d26: Remove usages of `EntityRef` and `parseEntityName` from `@backstage/catalog-model` +- 6e1cbc12a6: Updated according to the new `getEntityFacets` catalog API method +- b776ce5aab: Replaced use of deprecated `useEntityListProvider` hook with `useEntityList`. +- 0f37cdef19: Migrated over from the deprecated `spec.metadata` to `spec.templateInfo` for the `name` and the `baseUrl` of the template. +- 50e0242ac2: - Moved the `JSONSchema` type from `@backstage/catalog-model` to `JSONSchema7`. + - Renamed and prefixed some types ready for exporting. +- a2589000ee: - Reworking the `FieldExtensionComponentType` so we can export the `ui:schema:options` props in the `api-report.md`. + - Exporting all of the `UiOptions` types for the `FieldExtensions` so we can see them in the `api-report.md`. + - Removing the redundant type in the `CustomFieldValidator` union. +- 2f2543592c: You can now hide sections or fields in your templates based on a feature flag. For example, take this template: + + ```json + { + title: 'my-schema', + steps: [ + { + title: 'Fill in some steps', + schema: { + title: 'Fill in some steps', + 'backstage:featureFlag': 'experimental-feature', + properties: { + name: { + title: 'Name', + type: 'string', + 'backstage:featureFlag': 'should-show-some-stuff-first-option', + }, + description: { + title: 'Description', + type: 'string', + description: 'A description for the component', + }, + owner: { + title: 'Owner', + type: 'string', + description: 'Owner of the component', + }, + }, + type: 'object', + }, + }, + } + + ``` + + If you have a feature flag that is called `experimental-feature` then your first step would be shown if you that feature flag was not active then it wouldn't be shown. The same goes for the properties in the schema. Make sure to use the key `backstage:featureFlag` in your templates if you want to use this functionality. + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-scaffolder-common@0.2.2 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration@0.7.5 + - @backstage/integration-react@0.1.23 + - @backstage/plugin-permission-react@0.3.2 + +## 0.12.3 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 538ca90790: Use updated type names from `@backstage/catalog-client` +- deaf6065db: Adapt to the new `CatalogApi.getLocationByRef` +- e72d371296: Use `TemplateEntityV1beta2` from `@backstage/plugin-scaffolder-common` instead + of `@backstage/catalog-model`. +- Updated dependencies + - @backstage/plugin-scaffolder-common@0.2.0 + - @backstage/catalog-client@0.7.0 + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/integration-react@0.1.22 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/plugin-permission-react@0.3.1 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + - @backstage/types@0.1.2 + - @backstage/plugin-catalog-common@0.1.3 + ## 0.12.2 ### Patch Changes diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index f8ae64cd82..b51f18fcc9 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -8,153 +8,193 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { ComponentProps } from 'react'; import { ComponentType } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { Extension } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; import { FieldProps } from '@rjsf/core'; import { FieldValidation } from '@rjsf/core'; -import { IconButton } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; -import { JSONSchema } from '@backstage/catalog-model'; +import { JSONSchema7 } from 'json-schema'; +import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; import { default as React_2 } from 'react'; -import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; - -// Warning: (ae-missing-release-tag) "createScaffolderFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function createScaffolderFieldExtension( - options: FieldExtensionOptions, -): Extension<() => null>; +import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; // @public -export type CustomFieldValidator = - | ((data: T, field: FieldValidation) => void) - | (( - data: T, - field: FieldValidation, - context: { - apiHolder: ApiHolder; - }, - ) => void); +export function createScaffolderFieldExtension< + TReturnValue = unknown, + TInputProps = unknown, +>( + options: FieldExtensionOptions, +): Extension>; -// Warning: (ae-missing-release-tag) "EntityNamePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EntityNamePicker: ({ - schema: { title, description }, - ...props -}: FieldProps) => JSX.Element; +// @public +export type CustomFieldValidator = ( + data: TFieldReturnValue, + field: FieldValidation, + context: { + apiHolder: ApiHolder; + }, +) => void; // Warning: (ae-missing-release-tag) "EntityNamePickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityNamePickerFieldExtension: () => null; - -// Warning: (ae-missing-release-tag) "EntityPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EntityPicker: ({ - onChange, - schema: { title, description }, - required, - uiSchema, - rawErrors, - formData, - idSchema, -}: FieldProps) => JSX.Element; +export const EntityNamePickerFieldExtension: FieldExtensionComponent< + string, + {} +>; // Warning: (ae-missing-release-tag) "EntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityPickerFieldExtension: () => null; +export const EntityPickerFieldExtension: FieldExtensionComponent< + string, + EntityPickerUiOptions +>; -// @public -export const EntityTagsPicker: ({ - formData, - onChange, - uiSchema, -}: FieldProps) => JSX.Element; - -// @public -export const EntityTagsPickerFieldExtension: () => null; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "FavouriteTemplate" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "EntityPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // +// @public (undocumented) +export interface EntityPickerUiOptions { + // (undocumented) + allowArbitraryValues?: boolean; + // (undocumented) + allowedKinds?: string[]; + // (undocumented) + defaultKind?: string; +} + // @public -export const FavouriteTemplate: (props: Props) => JSX.Element; +export const EntityTagsPickerFieldExtension: FieldExtensionComponent< + string[], + EntityTagsPickerUiOptions +>; + +// Warning: (ae-missing-release-tag) "EntityTagsPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface EntityTagsPickerUiOptions { + // (undocumented) + kinds?: string[]; +} + +// @public +export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; // @public export interface FieldExtensionComponentProps< - ReturnValue, - UiOptions extends {} = {}, -> extends FieldProps { + TFieldReturnValue, + TUiOptions extends {} = {}, +> extends FieldProps { // (undocumented) - uiSchema: { - 'ui:options'?: UiOptions; + uiSchema: FieldProps['uiSchema'] & { + 'ui:options'?: TUiOptions; }; } // @public -export type FieldExtensionOptions = { +export type FieldExtensionOptions< + TFieldReturnValue = unknown, + TInputProps = unknown, +> = { name: string; - component: (props: FieldProps) => JSX.Element | null; - validation?: CustomFieldValidator; + component: ( + props: FieldExtensionComponentProps, + ) => JSX.Element | null; + validation?: CustomFieldValidator; }; -// Warning: (ae-missing-release-tag) "OwnedEntityPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "JobStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const OwnedEntityPicker: ({ - onChange, - schema: { title, description }, - required, - uiSchema, - rawErrors, - formData, - idSchema, -}: FieldProps) => JSX.Element; +export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; + +// Warning: (ae-missing-release-tag) "ListActionsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ListActionsResponse = Array<{ + id: string; + description?: string; + schema?: { + input?: JSONSchema7; + output?: JSONSchema7; + }; +}>; + +// Warning: (ae-missing-release-tag) "LogEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type LogEvent = { + type: 'log' | 'completion'; + body: { + message: string; + stepId?: string; + status?: ScaffolderTaskStatus; + }; + createdAt: string; + id: string; + taskId: string; +}; // Warning: (ae-missing-release-tag) "OwnedEntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const OwnedEntityPickerFieldExtension: () => null; +export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< + string, + OwnedEntityPickerUiOptions +>; -// Warning: (ae-missing-release-tag) "OwnerPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "OwnedEntityPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const OwnerPicker: ({ - schema: { title, description }, - uiSchema, - ...props -}: FieldProps) => JSX.Element; +export interface OwnedEntityPickerUiOptions { + // (undocumented) + allowedKinds?: string[]; + // (undocumented) + defaultKind?: string; +} // Warning: (ae-missing-release-tag) "OwnerPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const OwnerPickerFieldExtension: () => null; +export const OwnerPickerFieldExtension: FieldExtensionComponent< + string, + OwnerPickerUiOptions +>; -// Warning: (ae-missing-release-tag) "RepoUrlPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "OwnerPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const RepoUrlPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; +export interface OwnerPickerUiOptions { + // (undocumented) + allowedKinds?: string[]; +} + +// Warning: (ae-missing-release-tag) "repoPickerValidation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const repoPickerValidation: ( + value: string, + validation: FieldValidation, + context: { + apiHolder: ApiHolder; + }, +) => void; // Warning: (ae-missing-release-tag) "RepoUrlPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const RepoUrlPickerFieldExtension: () => null; +export const RepoUrlPickerFieldExtension: FieldExtensionComponent< + string, + RepoUrlPickerUiOptions +>; // Warning: (ae-missing-release-tag) "RepoUrlPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -176,37 +216,49 @@ export interface RepoUrlPickerUiOptions { }; } +// Warning: (ae-missing-release-tag) "RouterProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type RouterProps = { + TemplateCardComponent?: + | ComponentType<{ + template: TemplateEntityV1beta2; + }> + | undefined; + TaskPageComponent?: ComponentType<{}>; + components?: { + TemplateCardComponent?: + | ComponentType<{ + template: TemplateEntityV1beta2; + }> + | undefined; + TaskPageComponent?: ComponentType<{}>; + }; + groups?: Array<{ + title?: string; + titleComponent?: React_2.ReactNode; + filter: (entity: Entity) => boolean; + }>; +}; + // @public export interface ScaffolderApi { // (undocumented) - getIntegrationsList(options: { allowedHosts: string[] }): Promise< - { - type: string; - title: string; - host: string; - }[] - >; - // Warning: (ae-forgotten-export) The symbol "ScaffolderTask" needs to be exported by the entry point index.d.ts - // + getIntegrationsList( + options: ScaffolderGetIntegrationsListOptions, + ): Promise; // (undocumented) getTask(taskId: string): Promise; - // Warning: (ae-forgotten-export) The symbol "TemplateParameterSchema" needs to be exported by the entry point index.d.ts - // // (undocumented) getTemplateParameterSchema( - templateName: EntityName, + templateRef: string, ): Promise; - // Warning: (ae-forgotten-export) The symbol "ListActionsResponse" needs to be exported by the entry point index.d.ts listActions(): Promise; scaffold( - templateName: string, - values: Record, - secrets?: Record, - ): Promise; - // Warning: (ae-forgotten-export) The symbol "LogEvent" needs to be exported by the entry point index.d.ts - // + options: ScaffolderScaffoldOptions, + ): Promise; // (undocumented) - streamLogs(options: { taskId: string; after?: number }): Observable; + streamLogs(options: ScaffolderStreamLogsOptions): Observable; } // @public @@ -221,62 +273,56 @@ export class ScaffolderClient implements ScaffolderApi { useLongPollingLogs?: boolean; }); // (undocumented) - getIntegrationsList(options: { allowedHosts: string[] }): Promise< - { - type: string; - title: string; - host: string; - }[] - >; + getIntegrationsList( + options: ScaffolderGetIntegrationsListOptions, + ): Promise; // (undocumented) - getTask(taskId: string): Promise; + getTask(taskId: string): Promise; // (undocumented) getTemplateParameterSchema( - templateName: EntityName, + templateRef: string, ): Promise; // (undocumented) listActions(): Promise; scaffold( - templateName: string, - values: Record, - secrets?: Record, - ): Promise; + options: ScaffolderScaffoldOptions, + ): Promise; // (undocumented) - streamLogs(options: { taskId: string; after?: number }): Observable; + streamLogs(options: ScaffolderStreamLogsOptions): Observable; } -// Warning: (ae-missing-release-tag) "ScaffolderFieldExtensions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// @public +export const ScaffolderFieldExtensions: React_2.ComponentType; + +// Warning: (ae-missing-release-tag) "ScaffolderGetIntegrationsListOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ScaffolderFieldExtensions: React_2.ComponentType; +export interface ScaffolderGetIntegrationsListOptions { + // (undocumented) + allowedHosts: string[]; +} + +// Warning: (ae-missing-release-tag) "ScaffolderGetIntegrationsListResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ScaffolderGetIntegrationsListResponse { + // (undocumented) + integrations: { + type: string; + title: string; + host: string; + }[]; +} // Warning: (ae-missing-release-tag) "ScaffolderPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ScaffolderPage: ({ - TemplateCardComponent, - TaskPageComponent, - groups, -}: { - TemplateCardComponent?: - | ComponentType<{ - template: TemplateEntityV1beta2; - }> - | undefined; - TaskPageComponent?: ComponentType<{}> | undefined; - groups?: - | { - title?: string | undefined; - titleComponent?: ReactNode; - filter: (entity: Entity) => boolean; - }[] - | undefined; -}) => JSX.Element; +export const ScaffolderPage: (props: RouterProps) => JSX.Element; // Warning: (ae-missing-release-tag) "scaffolderPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const scaffolderPlugin: BackstagePlugin< +export const scaffolderPlugin: BackstagePlugin< { root: RouteRef; }, @@ -284,8 +330,76 @@ const scaffolderPlugin: BackstagePlugin< registerComponent: ExternalRouteRef; } >; -export { scaffolderPlugin as plugin }; -export { scaffolderPlugin }; + +// Warning: (ae-missing-release-tag) "ScaffolderScaffoldOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ScaffolderScaffoldOptions { + // (undocumented) + secrets?: Record; + // (undocumented) + templateRef: string; + // (undocumented) + values: Record; +} + +// Warning: (ae-missing-release-tag) "ScaffolderScaffoldResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ScaffolderScaffoldResponse { + // (undocumented) + taskId: string; +} + +// Warning: (ae-missing-release-tag) "ScaffolderStreamLogsOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ScaffolderStreamLogsOptions { + // (undocumented) + after?: number; + // (undocumented) + taskId: string; +} + +// Warning: (ae-missing-release-tag) "ScaffolderTask" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ScaffolderTask = { + id: string; + spec: TaskSpec; + status: 'failed' | 'completed' | 'processing' | 'open' | 'cancelled'; + lastHeartbeatAt: string; + createdAt: string; +}; + +// Warning: (ae-missing-release-tag) "ScaffolderTaskOutput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ScaffolderTaskOutput = { + entityRef?: string; + remoteUrl?: string; + links?: ScaffolderOutputLink[]; +} & { + [key: string]: unknown; +}; + +// Warning: (ae-missing-release-tag) "ScaffolderTaskStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ScaffolderTaskStatus = + | 'open' + | 'processing' + | 'failed' + | 'completed' + | 'skipped'; + +// @public +export interface ScaffolderUseTemplateSecrets { + // @deprecated (undocumented) + setSecret: (input: Record) => void; + // (undocumented) + setSecrets: (input: Record) => void; +} // @public export const TaskPage: ({ loadingText }: TaskPageProps) => JSX.Element; @@ -297,7 +411,7 @@ export type TaskPageProps = { // 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) +// @public @deprecated (undocumented) export const TemplateList: ({ TemplateCardComponent, group, @@ -305,7 +419,7 @@ export const TemplateList: ({ // Warning: (ae-missing-release-tag) "TemplateListProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type TemplateListProps = { TemplateCardComponent?: | ComponentType<{ @@ -313,33 +427,32 @@ export type TemplateListProps = { }> | undefined; group?: { - title?: string; + title?: React_2.ReactNode; titleComponent?: React_2.ReactNode; filter: (entity: Entity) => boolean; }; }; +// Warning: (ae-missing-release-tag) "TemplateParameterSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type TemplateParameterSchema = { + title: string; + steps: Array<{ + title: string; + schema: JsonObject; + }>; +}; + // Warning: (ae-missing-release-tag) "TemplateTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const TemplateTypePicker: () => JSX.Element | null; -// Warning: (ae-missing-release-tag) "TextValuePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const TextValuePicker: ({ - onChange, - required, - schema: { title, description }, - rawErrors, - formData, - uiSchema: { 'ui:autofocus': autoFocus }, - idSchema, - placeholder, -}: FieldProps) => JSX.Element; - // @public -export const useTemplateSecrets: () => { - setSecret: (input: Record) => void; -}; +export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets; + +// Warnings were encountered during analysis: +// +// src/types.d.ts:32:5 - (ae-forgotten-export) The symbol "ScaffolderOutputLink" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index 78e6254e09..03632f434d 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -20,16 +20,12 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { catalogApiRef, starredEntitiesApiRef, - DefaultStarredEntitiesApi, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; import { ScaffolderPage } from '../src/plugin'; -import { - discoveryApiRef, - fetchApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api'; import { CatalogEntityPage } from '@backstage/plugin-catalog'; createDevApp() @@ -44,8 +40,8 @@ createDevApp() }) .registerApi({ api: starredEntitiesApiRef, - deps: { storageApi: storageApiRef }, - factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), + deps: {}, + factory: () => new MockStarredEntitiesApi(), }) .registerApi({ api: scaffolderApiRef, diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 0e31d6c4a5..e4eada7a51 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.2", + "version": "0.13.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,30 +24,31 @@ "backstage" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.6.0", - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@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.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", + "@types/json-schema": "^7.0.9", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/integration-react": "^0.1.23", + "@backstage/plugin-catalog-common": "^0.1.4", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/plugin-scaffolder-common": "^0.2.2", + "@backstage/theme": "^0.2.15", + "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -69,11 +73,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@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", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/plugin-catalog": "^0.9.0", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index 369722ec8f..4e47c7f239 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -62,7 +62,9 @@ describe('api', () => { 'dev.azure.com', 'bitbucket.org', ]; - const integrations = await apiClient.getIntegrationsList({ allowedHosts }); + const { integrations } = await apiClient.getIntegrationsList({ + allowedHosts, + }); integrations.forEach(integration => expect(allowedHosts).toContain(integration.host), ); diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 62fc06a6db..818ee7971b 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { parseEntityRef } from '@backstage/catalog-model'; import { createApiRef, DiscoveryApi, @@ -22,11 +22,21 @@ import { } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { JsonObject, JsonValue, Observable } from '@backstage/types'; -import { Field, FieldValidation } from '@rjsf/core'; +import { Observable } from '@backstage/types'; import qs from 'qs'; import ObservableImpl from 'zen-observable'; -import { ListActionsResponse, ScaffolderTask, Status } from './types'; +import { + ListActionsResponse, + LogEvent, + ScaffolderApi, + TemplateParameterSchema, + ScaffolderScaffoldOptions, + ScaffolderScaffoldResponse, + ScaffolderStreamLogsOptions, + ScaffolderGetIntegrationsListOptions, + ScaffolderGetIntegrationsListResponse, + ScaffolderTask, +} from './types'; /** * Utility API reference for the {@link ScaffolderApi}. @@ -37,70 +47,6 @@ export const scaffolderApiRef = createApiRef({ id: 'plugin.scaffolder.service', }); -type TemplateParameterSchema = { - title: string; - steps: Array<{ - title: string; - schema: JsonObject; - }>; -}; - -export type LogEvent = { - type: 'log' | 'completion'; - body: { - message: string; - stepId?: string; - status?: Status; - }; - createdAt: string; - id: string; - taskId: string; -}; - -export type CustomField = { - name: string; - component: Field; - validation: (data: JsonValue, field: FieldValidation) => void; -}; - -/** - * An API to interact with the scaffolder backend. - * - * @public - */ -export interface ScaffolderApi { - getTemplateParameterSchema( - templateName: EntityName, - ): Promise; - - /** - * Executes the scaffolding of a component, given a template and its - * parameter values. - * - * @param templateName - Name of the Template entity for the scaffolder to use. New project is going to be created out of this template. - * @param values - Parameters for the template, e.g. name, description - * @param secrets - Optional secrets to pass to as the secrets parameter to the template. - */ - scaffold( - templateName: string, - values: Record, - secrets?: Record, - ): Promise; - - getTask(taskId: string): Promise; - - getIntegrationsList(options: { - allowedHosts: string[]; - }): Promise<{ type: string; title: string; host: string }[]>; - - /** - * Returns a list of all installed actions. - */ - listActions(): Promise; - - streamLogs(options: { taskId: string; after?: number }): Observable; -} - /** * An API to interact with the scaffolder backend. * @@ -124,8 +70,10 @@ export class ScaffolderClient implements ScaffolderApi { this.useLongPollingLogs = options.useLongPollingLogs ?? false; } - async getIntegrationsList(options: { allowedHosts: string[] }) { - return [ + async getIntegrationsList( + options: ScaffolderGetIntegrationsListOptions, + ): Promise { + const integrations = [ ...this.scmIntegrationsApi.azure.list(), ...this.scmIntegrationsApi.bitbucket.list(), ...this.scmIntegrationsApi.github.list(), @@ -133,17 +81,24 @@ export class ScaffolderClient implements ScaffolderApi { ] .map(c => ({ type: c.type, title: c.title, host: c.config.host })) .filter(c => options.allowedHosts.includes(c.host)); + + return { + integrations, + }; } async getTemplateParameterSchema( - templateName: EntityName, + templateRef: string, ): Promise { - const { namespace, kind, name } = templateName; + const { namespace, kind, name } = parseEntityRef(templateRef, { + defaultKind: 'template', + }); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const templatePath = [namespace, kind, name] .map(s => encodeURIComponent(s)) .join('/'); + const url = `${baseUrl}/v2/templates/${templatePath}/parameter-schema`; const response = await this.fetchApi.fetch(url); @@ -159,15 +114,12 @@ export class ScaffolderClient implements ScaffolderApi { * Executes the scaffolding of a component, given a template and its * parameter values. * - * @param templateName - Template name for the scaffolder to use. New project is going to be created out of this template. - * @param values - Parameters for the template, e.g. name, description - * @param secrets - Optional secrets to pass to as the secrets parameter to the template. + * @param options - The {@link ScaffolderScaffoldOptions} the scaffolding. */ async scaffold( - templateName: string, - values: Record, - secrets: Record = {}, - ): Promise { + options: ScaffolderScaffoldOptions, + ): Promise { + const { templateRef, values, secrets = {} } = options; const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`; const response = await this.fetchApi.fetch(url, { method: 'POST', @@ -175,7 +127,7 @@ export class ScaffolderClient implements ScaffolderApi { 'Content-Type': 'application/json', }, body: JSON.stringify({ - templateName, + templateRef, values: { ...values }, secrets, }), @@ -188,10 +140,10 @@ export class ScaffolderClient implements ScaffolderApi { } const { id } = (await response.json()) as { id: string }; - return id; + return { taskId: id }; } - async getTask(taskId: string) { + async getTask(taskId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`; @@ -203,10 +155,7 @@ export class ScaffolderClient implements ScaffolderApi { return await response.json(); } - streamLogs(options: { - taskId: string; - after?: number; - }): Observable { + streamLogs(options: ScaffolderStreamLogsOptions): Observable { if (this.useLongPollingLogs) { return this.streamLogsPolling(options); } diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index ce0354b817..aff6681037 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -14,11 +14,12 @@ * limitations under the License. */ import React from 'react'; -import { ScaffolderApi, scaffolderApiRef } from '../../api'; +import { scaffolderApiRef } from '../../api'; import { ActionsPage } from './ActionsPage'; import { rootRouteRef } from '../../routes'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { ApiProvider } from '@backstage/core-app-api'; +import { ScaffolderApi } from '../../types'; const scaffolderApiMock: jest.Mocked = { scaffold: jest.fn(), diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 4542979e73..78efaa43ff 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -28,8 +28,7 @@ import { TableRow, makeStyles, } from '@material-ui/core'; -import { JSONSchema } from '@backstage/catalog-model'; -import { JSONSchema7Definition } from 'json-schema'; +import { JSONSchema7, JSONSchema7Definition } from 'json-schema'; import classNames from 'classnames'; import { useApi } from '@backstage/core-plugin-api'; @@ -87,7 +86,7 @@ export const ActionsPage = () => { ); } - const formatRows = (input: JSONSchema) => { + const formatRows = (input: JSONSchema7) => { const properties = input.properties; if (!properties) { return undefined; @@ -95,7 +94,7 @@ export const ActionsPage = () => { return Object.entries(properties).map(entry => { const [key] = entry; - const props = entry[1] as unknown as JSONSchema; + const props = entry[1] as unknown as JSONSchema7; const codeClassname = classNames(classes.code, { [classes.codeRequired]: input.required?.includes(key), }); @@ -115,7 +114,7 @@ export const ActionsPage = () => { }); }; - const renderTable = (input: JSONSchema) => { + const renderTable = (input: JSONSchema7) => { if (!input.properties) { return undefined; } @@ -145,7 +144,7 @@ export const ActionsPage = () => { <> {name} {input.map((i, index) => ( -
{renderTable(i as unknown as JSONSchema)}
+
{renderTable(i as unknown as JSONSchema7)}
))} ); diff --git a/plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx b/plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx deleted file mode 100644 index 6e8fe45b2f..0000000000 --- a/plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx +++ /dev/null @@ -1,74 +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 { Entity } from '@backstage/catalog-model'; -import { useStarredEntity } from '@backstage/plugin-catalog-react'; -import { IconButton, makeStyles, Tooltip, withStyles } from '@material-ui/core'; -import Star from '@material-ui/icons/Star'; -import StarBorder from '@material-ui/icons/StarBorder'; -import React, { ComponentProps } from 'react'; - -type Props = ComponentProps & { entity: Entity }; - -const YellowStar = withStyles({ - root: { - color: '#f3ba37', - }, -})(Star); - -const WhiteBorderStar = withStyles({ - root: { - color: '#ffffff', - }, -})(StarBorder); - -const useStyles = makeStyles(theme => ({ - starButton: { - position: 'absolute', - top: theme.spacing(0.5), - right: theme.spacing(0.5), - padding: '0.25rem', - }, -})); - -export const favouriteTemplateTooltip = (isStarred: boolean) => - isStarred ? 'Remove from favorites' : 'Add to favorites'; - -export const favouriteTemplateIcon = (isStarred: boolean) => - isStarred ? : ; - -/** - * IconButton for showing if a current entity is starred and adding/removing it from the favourite entities - * @param props - MaterialUI IconButton props extended by required `entity` prop - */ -export const FavouriteTemplate = (props: Props) => { - const classes = useStyles(); - const { toggleStarredEntity, isStarredEntity } = useStarredEntity( - props.entity, - ); - return ( - toggleStarredEntity()} - > - - {favouriteTemplateIcon(isStarredEntity)} - - - ); -}; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index a9c8eaec1d..4d37eafee4 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -24,12 +24,17 @@ import { Stepper, Typography, } from '@material-ui/core'; -import { errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { + errorApiRef, + useApi, + featureFlagsApiRef, +} from '@backstage/core-plugin-api'; import { FormProps, IChangeEvent, UiSchema, withTheme } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { useState } from 'react'; import { transformSchemaToProps } from './schema'; import { Content, StructuredMetadataTable } from '@backstage/core-components'; +import cloneDeep from 'lodash/cloneDeep'; import * as fieldOverrides from './FieldOverrides'; const Form = withTheme(MuiTheme); @@ -104,18 +109,50 @@ export function getReviewData(formData: Record, steps: Step[]) { return reviewData; } -export const MultistepJsonForm = ({ - steps, - formData, - onChange, - onReset, - onFinish, - fields, - widgets, -}: Props) => { +export const MultistepJsonForm = (props: Props) => { + const { formData, onChange, onReset, onFinish, fields, widgets } = props; const [activeStep, setActiveStep] = useState(0); const [disableButtons, setDisableButtons] = useState(false); const errorApi = useApi(errorApiRef); + const featureFlagApi = useApi(featureFlagsApiRef); + const featureFlagKey = 'backstage:featureFlag'; + const filterOutProperties = (step: Step): Step => { + const filteredStep = cloneDeep(step); + const removedPropertyKeys: Array = []; + if (filteredStep.schema.properties) { + filteredStep.schema.properties = Object.fromEntries( + Object.entries(filteredStep.schema.properties).filter( + ([key, value]) => { + if (value[featureFlagKey]) { + if (featureFlagApi.isActive(value[featureFlagKey])) { + return true; + } + removedPropertyKeys.push(key); + return false; + } + return true; + }, + ), + ); + + // remove the feature flag property key from required if they are not active + filteredStep.schema.required = Array.isArray(filteredStep.schema.required) + ? filteredStep.schema.required?.filter( + r => !removedPropertyKeys.includes(r as string), + ) + : filteredStep.schema.required; + } + return filteredStep; + }; + + const steps = props.steps + .filter(step => { + const featureFlag = step.schema[featureFlagKey]; + return ( + typeof featureFlag !== 'string' || featureFlagApi.isActive(featureFlag) + ); + }) + .map(filterOutProperties); const handleReset = () => { setActiveStep(0); diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 601844780d..a456f2bdd3 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -16,7 +16,8 @@ import React, { ComponentType } from 'react'; import { Routes, Route, useOutlet } from 'react-router'; -import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; @@ -31,11 +32,20 @@ import { } from '../extensions'; import { useElementFilter } from '@backstage/core-plugin-api'; -type RouterProps = { +export type RouterProps = { + /** @deprecated use components.TemplateCardComponent instead */ TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; + /** @deprecated use component.TaskPageComponent instead */ TaskPageComponent?: ComponentType<{}>; + + components?: { + TemplateCardComponent?: + | ComponentType<{ template: TemplateEntityV1beta2 }> + | undefined; + TaskPageComponent?: ComponentType<{}>; + }; groups?: Array<{ title?: string; titleComponent?: React.ReactNode; @@ -43,13 +53,26 @@ type RouterProps = { }>; }; -export const Router = ({ - TemplateCardComponent, - TaskPageComponent, - groups, -}: RouterProps) => { +export const Router = (props: RouterProps) => { + const { + TemplateCardComponent: legacyTemplateCardComponent, + TaskPageComponent: legacyTaskPageComponent, + groups, + components = {}, + } = props; + + if (legacyTemplateCardComponent || legacyTaskPageComponent) { + // eslint-disable-next-line no-console + console.warn( + "DEPRECATION: 'TemplateCardComponent' and 'TaskPageComponent' are deprecated when calling the 'ScaffolderPage'. Use 'components' prop to pass these component overrides instead.", + ); + } + + const { TemplateCardComponent, TaskPageComponent } = components; + const outlet = useOutlet(); - const TaskPageElement = TaskPageComponent || TaskPage; + const TaskPageElement = + TaskPageComponent ?? legacyTaskPageComponent ?? TaskPage; const customFieldExtensions = useElementFilter(outlet, elements => elements @@ -77,8 +100,10 @@ export const Router = ({ path="/" element={ } /> diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 116b7ed82a..da9f1f74b8 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -23,7 +23,8 @@ import { Page, SupportButton, } from '@backstage/core-components'; -import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; import { useRouteRef } from '@backstage/core-plugin-api'; import { EntityKindPicker, @@ -54,7 +55,8 @@ export type ScaffolderPageProps = { | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; groups?: Array<{ - title?: string; + title?: React.ReactNode; + /** @deprcated use title instead as it accepts a string and react component */ titleComponent?: React.ReactNode; filter: (entity: Entity) => boolean; }>; diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 2f49f40643..e0445ed641 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { parseEntityRef } from '@backstage/catalog-model'; import { Content, ErrorPage, @@ -45,10 +46,10 @@ import classNames from 'classnames'; import { DateTime, Interval } from 'luxon'; import qs from 'qs'; import React, { memo, useEffect, useMemo, useState } from 'react'; -import { generatePath, useNavigate, useParams } from 'react-router'; +import { useNavigate, useParams } from 'react-router'; import useInterval from 'react-use/lib/useInterval'; -import { rootRouteRef } from '../../routes'; -import { Status, TaskOutput } from '../../types'; +import { rootRouteRef, selectedTemplateRouteRef } from '../../routes'; +import { ScaffolderTaskStatus, ScaffolderTaskOutput } from '../../types'; import { useTaskEventStream } from '../hooks/useEventStream'; import { TaskPageLinks } from './TaskPageLinks'; @@ -85,7 +86,7 @@ const useStyles = makeStyles((theme: Theme) => type TaskStep = { id: string; name: string; - status: Status; + status: ScaffolderTaskStatus; startedAt?: string; endedAt?: string; }; @@ -216,7 +217,11 @@ export const TaskStatusStepper = memo( }, ); -const hasLinks = ({ entityRef, remoteUrl, links = [] }: TaskOutput): boolean => +const hasLinks = ({ + entityRef, + remoteUrl, + links = [], +}: ScaffolderTaskOutput): boolean => !!(entityRef || remoteUrl || links.length > 0); /** @@ -238,7 +243,8 @@ export type TaskPageProps = { export const TaskPage = ({ loadingText }: TaskPageProps) => { const classes = useStyles(); const navigate = useNavigate(); - const rootLink = useRouteRef(rootRouteRef); + const rootPath = useRouteRef(rootRouteRef); + const templateRoute = useRouteRef(selectedTemplateRouteRef); const [userSelectedStepId, setUserSelectedStepId] = useState< string | undefined >(undefined); @@ -291,8 +297,9 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => { const { output } = taskStream; const handleStartOver = () => { - if (!taskStream.task || !taskStream.task?.spec.metadata?.name) { - navigate(generatePath(rootLink())); + if (!taskStream.task || !taskStream.task?.spec.templateInfo?.entityRef) { + navigate(rootPath()); + return; } const formData = @@ -300,15 +307,14 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => { ? taskStream.task!.spec.values : taskStream.task!.spec.parameters; + const { name } = parseEntityRef( + taskStream.task!.spec.templateInfo?.entityRef, + ); + navigate( - generatePath( - `${rootLink()}/templates/:templateName?${qs.stringify({ - formData: JSON.stringify(formData), - })}`, - { - templateName: taskStream.task!.spec.metadata!.name, - }, - ), + `${templateRoute({ templateName: name })}?${qs.stringify({ + formData: JSON.stringify(formData), + })}`, ); }; diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx index 2adec564a1..5451b4e725 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx @@ -14,17 +14,17 @@ * limitations under the License. */ -import { parseEntityName } from '@backstage/catalog-model'; +import { parseEntityRef } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import LanguageIcon from '@material-ui/icons/Language'; import React from 'react'; -import { TaskOutput } from '../../types'; +import { ScaffolderTaskOutput } from '../../types'; import { IconLink } from './IconLink'; import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api'; type TaskPageLinksProps = { - output: TaskOutput; + output: ScaffolderTaskOutput; }; export const TaskPageLinks = ({ output }: TaskPageLinksProps) => { @@ -57,7 +57,7 @@ export const TaskPageLinks = ({ output }: TaskPageLinksProps) => { .filter(({ url, entityRef }) => url || entityRef) .map(({ url, entityRef, title, icon }) => { if (entityRef) { - const entityName = parseEntityName(entityRef); + const entityName = parseEntityRef(entityRef); const target = entityRoute(entityName); return { title, icon, url: target }; } diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index a02e6d3b14..3a7691dd5a 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -13,17 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - Entity, - RELATION_OWNED_BY, - TemplateEntityV1beta2, -} from '@backstage/catalog-model'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrationIcon, scmIntegrationsApiRef, } from '@backstage/integration-react'; import { EntityRefLinks, + FavoriteEntity, getEntityRelations, getEntitySourceLocation, } from '@backstage/plugin-catalog-react'; @@ -44,9 +42,7 @@ import { } from '@material-ui/core'; import WarningIcon from '@material-ui/icons/Warning'; import React from 'react'; -import { generatePath } from 'react-router'; -import { rootRouteRef } from '../../routes'; -import { FavouriteTemplate } from '../FavouriteTemplate/FavouriteTemplate'; +import { selectedTemplateRouteRef } from '../../routes'; import { Button, ItemCardHeader } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; @@ -78,6 +74,12 @@ const useStyles = makeStyles(theme => ({ leftButton: { marginRight: 'auto', }, + starButton: { + position: 'absolute', + top: theme.spacing(0.5), + right: theme.spacing(0.5), + padding: '0.25rem', + }, })); const useDeprecationStyles = makeStyles(theme => ({ @@ -123,7 +125,8 @@ const DeprecationWarning = () => { const Title = ( - This template syntax is deprecated. Click for more info. + This template uses a syntax that has been deprecated, and should be + migrated to a newer syntax. Click for more info. ); @@ -131,7 +134,7 @@ const DeprecationWarning = () => {
@@ -143,7 +146,7 @@ const DeprecationWarning = () => { export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { const backstageTheme = useTheme(); - const rootLink = useRouteRef(rootRouteRef); + const templateRoute = useRouteRef(selectedTemplateRouteRef); const templateProps = getTemplateCardProps(template); const ownedByRelations = getEntityRelations( template as Entity, @@ -154,9 +157,7 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { : 'other'; const theme = backstageTheme.getPageTheme({ themeId }); const classes = useStyles({ backgroundImage: theme.backgroundImage }); - const href = generatePath(`${rootLink()}/templates/:templateName`, { - templateName: templateProps.name, - }); + const href = templateRoute({ templateName: templateProps.name }); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); const sourceLocation = getEntitySourceLocation(template, scmIntegrationsApi); @@ -164,7 +165,7 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { return ( - + {deprecated && } | undefined; group?: { - title?: string; + title?: React.ReactNode; + /** @deprecated use title instead, can be a string or a react component */ titleComponent?: React.ReactNode; filter: (entity: Entity) => boolean; }; }; +/** + * @deprecated this component is deprecated and will be removed in a future releases, please use the TemplateCard to render your own list. + */ export const TemplateList = ({ TemplateCardComponent, group, }: TemplateListProps) => { - const { loading, error, entities } = useEntityListProvider(); + const { loading, error, entities } = useEntityList(); const Card = TemplateCardComponent || TemplateCard; const maybeFilteredEntities = group ? entities.filter(e => group.filter(e)) : entities; - const title = group ? ( - group.titleComponent || - ) : ( - - ); + + const titleComponent: React.ReactNode = (() => { + if (group?.titleComponent) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATED: group.titleComponent is now deprecated. Use group.title instead, it can be a string or a react component', + ); + return group?.titleComponent; + } + if (group && group.title) { + if (typeof group.title === 'string') { + return ; + } + return group.title; + } + + return ; + })(); if (group && maybeFilteredEntities.length === 0) { return null; @@ -82,7 +99,7 @@ export const TemplateList = ({ )} - {title} + {titleComponent} {maybeFilteredEntities && maybeFilteredEntities?.length > 0 && @@ -90,6 +107,7 @@ export const TemplateList = ({ ))} diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index 7ca65fff25..d0eca58b9d 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -23,9 +23,14 @@ import { ThemeProvider } from '@material-ui/core'; import { act, fireEvent, within } from '@testing-library/react'; import React from 'react'; import { MemoryRouter, Route } from 'react-router'; -import { ScaffolderApi, scaffolderApiRef } from '../../api'; +import { scaffolderApiRef } from '../../api'; +import { ScaffolderApi } from '../../types'; import { rootRouteRef } from '../../routes'; import { TemplatePage } from './TemplatePage'; +import { + featureFlagsApiRef, + FeatureFlagsApi, +} from '@backstage/core-plugin-api'; import { ApiProvider } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; @@ -48,11 +53,63 @@ const scaffolderApiMock: jest.Mocked = { listActions: jest.fn(), }; +const featureFlagsApiMock: jest.Mocked = { + isActive: jest.fn(), + registerFlag: jest.fn(), + getRegisteredFlags: jest.fn(), + save: jest.fn(), +}; + const errorApiMock = { post: jest.fn(), error$: jest.fn() }; +const schemaMockValue = { + title: 'my-schema', + steps: [ + { + title: 'Fill in some steps', + schema: { + title: 'Fill in some steps', + 'backstage:featureFlag': 'experimental-feature', + properties: { + name: { + title: 'Name', + type: 'string', + 'backstage:featureFlag': 'should-show-some-stuff-first-option', + }, + description: { + title: 'Description', + type: 'string', + description: 'A description for the component', + }, + owner: { + title: 'Owner', + type: 'string', + description: 'Owner of the component', + }, + }, + type: 'object', + }, + }, + { + title: 'Send data', + schema: { + title: 'Send data', + properties: { + user: { + title: 'User', + type: 'string', + }, + }, + type: 'object', + }, + }, + ], +}; + const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [errorApiRef, errorApiMock], + [featureFlagsApiRef, featureFlagsApiMock], ); describe('TemplatePage', () => { @@ -197,4 +254,32 @@ describe('TemplatePage', () => { fireEvent.click(await findByText('Next step')); expect(await findByText('Reset')).toBeInTheDocument(); }); + + it('should display a section or property based on a feature flag', async () => { + featureFlagsApiMock.isActive.mockImplementation(flag => { + if (flag === 'experimental-feature') { + return true; + } + return false; + }); + scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue( + schemaMockValue, + ); + + const { queryByText } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/create/actions': rootRouteRef, + }, + }, + ); + + expect(await queryByText('Name')).not.toBeInTheDocument(); + expect(await queryByText('Description')).toBeInTheDocument(); + expect(await queryByText('Owner')).toBeInTheDocument(); + expect(await queryByText('Send data')).toBeInTheDocument(); + }); }); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index d30c292053..5310a4c79f 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -18,13 +18,13 @@ import { LinearProgress } from '@material-ui/core'; import { FormValidation, IChangeEvent } from '@rjsf/core'; import qs from 'qs'; import React, { useCallback, useContext, useState } from 'react'; -import { generatePath, Navigate, useNavigate } from 'react-router'; +import { 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 { rootRouteRef, scaffolderTaskRouteRef } from '../../routes'; import { MultistepJsonForm } from '../MultistepJsonForm'; import { @@ -41,17 +41,13 @@ import { useApiHolder, useRouteRef, } from '@backstage/core-plugin-api'; +import { stringifyEntityRef } from '@backstage/catalog-model'; -const useTemplateParameterSchema = (templateName: string) => { +const useTemplateParameterSchema = (templateRef: string) => { const scaffolderApi = useApi(scaffolderApiRef); const { value, loading, error } = useAsync( - () => - scaffolderApi.getTemplateParameterSchema({ - name: templateName, - kind: 'template', - namespace: 'default', - }), - [scaffolderApi, templateName], + () => scaffolderApi.getTemplateParameterSchema(templateRef), + [scaffolderApi, templateRef], ); return { schema: value, loading, error }; }; @@ -113,7 +109,7 @@ export const createValidator = ( export const TemplatePage = ({ customFieldExtensions = [], }: { - customFieldExtensions?: FieldExtensionOptions[]; + customFieldExtensions?: FieldExtensionOptions[]; }) => { const apiHolder = useApiHolder(); const secretsContext = useContext(SecretsContext); @@ -121,7 +117,8 @@ export const TemplatePage = ({ const scaffolderApi = useApi(scaffolderApiRef); const { templateName } = useParams(); const navigate = useNavigate(); - const rootLink = useRouteRef(rootRouteRef); + const scaffolderTaskRoute = useRouteRef(scaffolderTaskRouteRef); + const rootRoute = useRouteRef(rootRouteRef); const { schema, loading, error } = useTemplateParameterSchema(templateName); const [formState, setFormState] = useState>(() => { const query = qs.parse(window.location.search, { @@ -141,11 +138,15 @@ export const TemplatePage = ({ ); const handleCreate = async () => { - const id = await scaffolderApi.scaffold( - templateName, - formState, - secretsContext?.secrets, - ); + const { taskId } = await scaffolderApi.scaffold({ + templateRef: stringifyEntityRef({ + name: templateName, + kind: 'template', + namespace: 'default', + }), + values: formState, + secrets: secretsContext?.secrets, + }); const formParams = qs.stringify( { formData: formState }, @@ -158,16 +159,16 @@ export const TemplatePage = ({ // extra back/forward slots. window.history?.replaceState(null, document.title, newUrl); - navigate(generatePath(`${rootLink()}/tasks/:taskId`, { taskId: id })); + navigate(scaffolderTaskRoute({ taskId })); }; if (error) { errorApi.post(new Error(`Failed to load template, ${error}`)); - return ; + return ; } if (!loading && !schema) { errorApi.post(new Error('Template was not found.')); - return ; + return ; } const customFieldComponents = Object.fromEntries( diff --git a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx index c70f251eac..aa6630d4aa 100644 --- a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx +++ b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx @@ -17,7 +17,6 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import { capitalize } from 'lodash'; -import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { TemplateTypePicker } from './TemplateTypePicker'; import { @@ -28,6 +27,7 @@ import { import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider } from '@backstage/core-app-api'; import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; +import { GetEntityFacetsResponse } from '@backstage/catalog-client'; const entities: Entity[] = [ { @@ -66,10 +66,15 @@ const apis = TestApiRegistry.from( [ catalogApiRef, { - getEntities: jest - .fn() - .mockImplementation(() => Promise.resolve({ items: entities })), - } as unknown as CatalogApi, + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { + 'spec.type': entities.map(e => ({ + value: (e.spec as any).type, + count: 1, + })), + }, + } as GetEntityFacetsResponse), + }, ], [ alertApiRef, diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx index 8e7c0b0bea..802135a863 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx @@ -14,12 +14,38 @@ * limitations under the License. */ import React from 'react'; -import { FieldProps } from '@rjsf/core'; -import { TextValuePicker } from '../TextValuePicker'; +import { FieldExtensionComponentProps } from '../../../extensions'; +import { TextField } from '@material-ui/core'; -export const EntityNamePicker = ({ - schema: { title = 'Name', description = 'Unique name of the component' }, - ...props -}: FieldProps) => ( - -); +/** + * EntityName Picker + */ +export const EntityNamePicker = ( + props: FieldExtensionComponentProps, +) => { + const { + onChange, + required, + schema: { title = 'Name', description = 'Unique name of the component' }, + rawErrors, + formData, + uiSchema: { 'ui:autofocus': autoFocus }, + idSchema, + placeholder, + } = props; + + return ( + onChange(value)} + margin="normal" + error={rawErrors?.length > 0 && !formData} + inputProps={{ autoFocus }} + /> + ); +}; diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts index 0ba88d1277..7076221480 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityNamePicker } from './EntityNamePicker'; export { entityNamePickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx index 3dc609a63b..942eef9d82 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx @@ -18,9 +18,9 @@ import { Entity } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { FieldProps } from '@rjsf/core'; -import userEvent from '@testing-library/user-event'; +import { fireEvent } from '@testing-library/react'; import React from 'react'; -import { EntityPicker, allowArbitraryValues } from './EntityPicker'; +import { EntityPicker } from './EntityPicker'; const makeEntity = (kind: string, namespace: string, name: string): Entity => ({ apiVersion: 'backstage.io/v1beta1', @@ -46,7 +46,7 @@ describe('', () => { getEntityByName: jest.fn(), getEntities: jest.fn(async () => ({ items: entities })), addLocation: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; let Wrapper: React.ComponentType; @@ -92,15 +92,16 @@ describe('', () => { }); it('updates even if there is not an exact match', async () => { - const { getByLabelText } = await renderInTestApp( + const { getByRole } = await renderInTestApp( , ); - const input = getByLabelText('Entity'); - userEvent.type(input, 'squ'); - input.blur(); + const input = getByRole('textbox'); + + fireEvent.change(input, { target: { value: 'squ' } }); + fireEvent.blur(input); expect(onChange).toHaveBeenCalledWith('squ'); }); @@ -136,37 +137,3 @@ describe('', () => { }); }); }); - -describe('allowArbitraryValues', () => { - describe('without ui:options', () => { - it('defaults to true', () => { - const uiSchema = {}; - const result = allowArbitraryValues(uiSchema); - expect(result).toBe(true); - }); - }); - - describe('without ui:options.allowArbitraryValues', () => { - it('defaults to true', () => { - const uiSchema = { 'ui:options': {} }; - const result = allowArbitraryValues(uiSchema); - expect(result).toBe(true); - }); - }); - - describe('with ui:options.allowArbitraryValues set to true', () => { - it('is true', () => { - const uiSchema = { 'ui:options': { allowArbitraryValues: true } }; - const result = allowArbitraryValues(uiSchema); - expect(result).toBe(true); - }); - }); - - describe('with ui:options.allowArbitraryValues set to false', () => { - it('is false', () => { - const uiSchema = { 'ui:options': { allowArbitraryValues: false } }; - const result = allowArbitraryValues(uiSchema); - expect(result).toBe(false); - }); - }); -}); diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index 959e7883c0..4a2f212fee 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -16,29 +16,39 @@ import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { TextField } from '@material-ui/core'; import FormControl from '@material-ui/core/FormControl'; import Autocomplete from '@material-ui/lab/Autocomplete'; -import { FieldProps, UiSchema } from '@rjsf/core'; import React, { useCallback, useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; +import { FieldExtensionComponentProps } from '../../../extensions'; -export const allowArbitraryValues = (uiSchema: UiSchema): boolean => - (uiSchema['ui:options']?.allowArbitraryValues as boolean) ?? true; +export interface EntityPickerUiOptions { + allowedKinds?: string[]; + defaultKind?: string; + allowArbitraryValues?: boolean; +} + +/** + * Entity Picker + */ +export const EntityPicker = ( + props: FieldExtensionComponentProps, +) => { + const { + onChange, + schema: { title = 'Entity', description = 'An entity from the catalog' }, + required, + uiSchema, + rawErrors, + formData, + idSchema, + } = props; + const allowedKinds = uiSchema['ui:options']?.allowedKinds; + const defaultKind = uiSchema['ui:options']?.defaultKind; -export const EntityPicker = ({ - onChange, - schema: { title = 'Entity', description = 'An entity from the catalog' }, - required, - uiSchema, - rawErrors, - formData, - idSchema, -}: FieldProps) => { - const allowedKinds = uiSchema['ui:options']?.allowedKinds as string[]; - const defaultKind = uiSchema['ui:options']?.defaultKind as string | undefined; const catalogApi = useApi(catalogApiRef); const { value: entities, loading } = useAsync(() => @@ -48,7 +58,7 @@ export const EntityPicker = ({ ); const entityRefs = entities?.items.map(e => - formatEntityRefTitle(e, { defaultKind }), + humanizeEntityRef(e, { defaultKind }), ); const onSelect = useCallback( @@ -78,7 +88,7 @@ export const EntityPicker = ({ onChange={onSelect} options={entityRefs || []} autoSelect - freeSolo={allowArbitraryValues(uiSchema)} + freeSolo={uiSchema['ui:options']?.allowArbitraryValues ?? true} renderInput={params => ( ) => { +export const EntityTagsPicker = ( + props: FieldExtensionComponentProps, +) => { + const { formData, onChange, uiSchema } = props; const catalogApi = useApi(catalogApiRef); const [inputValue, setInputValue] = useState(''); const [inputError, setInputError] = useState(false); const tagValidator = makeValidator().isValidTag; - const kinds = uiSchema['ui:options']?.kinds as string[]; + const kinds = uiSchema['ui:options']?.kinds; 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/EntityTagsPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts index 09f4d5c519..9ff6e553a6 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityTagsPicker } from './EntityTagsPicker'; +export type { EntityTagsPickerUiOptions } from './EntityTagsPicker'; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index 3c2fc2b495..aded9acffa 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -14,30 +14,43 @@ * limitations under the License. */ import { - formatEntityRefTitle, + humanizeEntityRef, useOwnedEntities, } from '@backstage/plugin-catalog-react'; import { TextField } from '@material-ui/core'; import FormControl from '@material-ui/core/FormControl'; import Autocomplete from '@material-ui/lab/Autocomplete'; -import { FieldProps } from '@rjsf/core'; import React from 'react'; -export const OwnedEntityPicker = ({ - onChange, - schema: { title = 'Entity', description = 'An entity from the catalog' }, - required, - uiSchema, - rawErrors, - formData, - idSchema, -}: FieldProps) => { - const allowedKinds = uiSchema['ui:options']?.allowedKinds as string[]; - const defaultKind = uiSchema['ui:options']?.defaultKind as string | undefined; +import { FieldExtensionComponentProps } from '../../../extensions'; + +export interface OwnedEntityPickerUiOptions { + allowedKinds?: string[]; + defaultKind?: string; +} + +/** + * Owned Entity Picker + */ +export const OwnedEntityPicker = ( + props: FieldExtensionComponentProps, +) => { + const { + onChange, + schema: { title = 'Entity', description = 'An entity from the catalog' }, + required, + uiSchema, + rawErrors, + formData, + idSchema, + } = props; + + const allowedKinds = uiSchema['ui:options']?.allowedKinds; + const defaultKind = uiSchema['ui:options']?.defaultKind; const { ownedEntities, loading } = useOwnedEntities(allowedKinds); const entityRefs = ownedEntities?.items - .map(e => formatEntityRefTitle(e, { defaultKind })) + .map(e => humanizeEntityRef(e, { defaultKind })) .filter(n => n); const onSelect = (_: any, value: string | null) => { diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts index 7cf5add6eb..2988ba8cdc 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { OwnedEntityPicker } from './OwnedEntityPicker'; +export type { OwnedEntityPickerUiOptions } from './OwnedEntityPicker'; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx index 31992fbc8a..b78d3add9e 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx @@ -43,7 +43,7 @@ describe('', () => { getEntityByName: jest.fn(), getEntities: jest.fn(async () => ({ items: entities })), addLocation: jest.fn(), - getLocationByEntity: jest.fn(), + getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; let Wrapper: React.ComponentType; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx index dd02622ae9..1824275380 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx @@ -13,15 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { FieldProps } from '@rjsf/core'; import React from 'react'; -import { EntityPicker } from '../EntityPicker'; +import { EntityPicker } from '../EntityPicker/EntityPicker'; +import { FieldExtensionComponentProps } from '../../../extensions'; + +export interface OwnerPickerUiOptions { + allowedKinds?: string[]; +} + +/** + * Owner Picker + */ +export const OwnerPicker = ( + props: FieldExtensionComponentProps, +) => { + const { + schema: { title = 'Owner', description = 'The owner of the component' }, + uiSchema, + ...restProps + } = props; -export const OwnerPicker = ({ - schema: { title = 'Owner', description = 'The owner of the component' }, - uiSchema, - ...props -}: FieldProps) => { const ownerUiSchema = { ...uiSchema, 'ui:options': { @@ -35,7 +46,7 @@ export const OwnerPicker = ({ return ( diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts index 801ed59c93..aa26024b5b 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { OwnerPicker } from './OwnerPicker'; +export type { OwnerPickerUiOptions } from './OwnerPicker'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx index 829495cdda..22043d5761 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx @@ -23,7 +23,9 @@ import { scmAuthApiRef, ScmAuthApi, } from '@backstage/integration-react'; -import { scaffolderApiRef, ScaffolderApi } from '../../../api'; +import { scaffolderApiRef } from '../../../api'; +import { ScaffolderApi } from '../../../types'; + import { SecretsContextProvider, SecretsContext, @@ -32,10 +34,12 @@ 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' }, - ], + getIntegrationsList: async () => ({ + integrations: [ + { host: 'github.com', type: 'github', title: 'github.com' }, + { host: 'dev.azure.com', type: 'azure', title: 'dev.azure.com' }, + ], + }), }; const mockIntegrationsApi: Partial = { diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 1ccaed4e86..17fd5ef665 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -44,6 +44,9 @@ export interface RepoUrlPickerUiOptions { }; } +/** + * Repo Url Picker + */ export const RepoUrlPicker = ( props: FieldExtensionComponentProps, ) => { @@ -53,7 +56,7 @@ export const RepoUrlPicker = ( ); const integrationApi = useApi(scmIntegrationsApiRef); const scmAuthApi = useApi(scmAuthApiRef); - const { setSecret } = useTemplateSecrets(); + const { setSecrets } = useTemplateSecrets(); const allowedHosts = useMemo( () => uiSchema?.['ui:options']?.allowedHosts ?? [], [uiSchema], @@ -111,7 +114,7 @@ export const RepoUrlPicker = ( // 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 }); + setSecrets({ [requestUserCredentials.secretsKey]: token }); }, 500, [state, uiSchema], diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.test.tsx index e151ddbbc7..bd71fb160f 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.test.tsx @@ -23,11 +23,11 @@ describe('RepoUrlPickerHostField', () => { it('renders the default host properly', async () => { const mockOnChange = jest.fn(); const mockScaffolderApi = { - getIntegrationsList: jest - .fn() - .mockResolvedValue([ + getIntegrationsList: jest.fn().mockResolvedValue({ + integrations: [ { host: 'github.com', title: 'github.com', type: 'github' }, - ]), + ], + }), }; const { getByText } = await renderInTestApp( @@ -46,10 +46,12 @@ describe('RepoUrlPickerHostField', () => { it('should provide a dropdown when multiple hosts are returned that can be selected', async () => { const mockOnChange = jest.fn(); const mockScaffolderApi = { - getIntegrationsList: jest.fn().mockResolvedValue([ - { host: 'github.com', title: 'github.com', type: 'github' }, - { host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' }, - ]), + getIntegrationsList: jest.fn().mockResolvedValue({ + integrations: [ + { host: 'github.com', title: 'github.com', type: 'github' }, + { host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' }, + ], + }), }; const { getByRole, getByText, getByTestId } = await renderInTestApp( @@ -73,10 +75,12 @@ describe('RepoUrlPickerHostField', () => { it('should not display hosts that dont have integration config set correctly', async () => { const mockOnChange = jest.fn(); const mockScaffolderApi = { - getIntegrationsList: jest.fn().mockResolvedValue([ - { host: 'github.com', title: 'github.com', type: 'github' }, - { host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' }, - ]), + getIntegrationsList: jest.fn().mockResolvedValue({ + integrations: [ + { host: 'github.com', title: 'github.com', type: 'github' }, + { host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' }, + ], + }), }; const { getByRole, getByText, getByTestId } = await renderInTestApp( diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx index ec09494a1b..b9762cd69d 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx @@ -30,11 +30,13 @@ export const RepoUrlPickerHost = (props: { const { host, hosts, onChange, rawErrors } = props; const scaffolderApi = useApi(scaffolderApiRef); - const { value: integrations, loading } = useAsync(async () => { - return await scaffolderApi.getIntegrationsList({ - allowedHosts: hosts ?? [], - }); - }); + const { value: { integrations } = { integrations: [] }, loading } = useAsync( + async () => { + return await scaffolderApi.getIntegrationsList({ + allowedHosts: hosts ?? [], + }); + }, + ); useEffect(() => { // If there is no host chosen currently diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index 2fdbf0aac9..c5f596a786 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -13,6 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { RepoUrlPicker } from './RepoUrlPicker'; export type { RepoUrlPickerUiOptions } from './RepoUrlPicker'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/TextValuePicker/TextValuePicker.tsx b/plugins/scaffolder/src/components/fields/TextValuePicker/TextValuePicker.tsx deleted file mode 100644 index 7bed7eedb9..0000000000 --- a/plugins/scaffolder/src/components/fields/TextValuePicker/TextValuePicker.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { TextField } from '@material-ui/core'; -import { FieldProps } from '@rjsf/core'; -import React from 'react'; - -export const TextValuePicker = ({ - onChange, - required, - schema: { title, description }, - rawErrors, - formData, - uiSchema: { 'ui:autofocus': autoFocus }, - idSchema, - placeholder, -}: FieldProps) => ( - onChange(value)} - margin="normal" - error={rawErrors?.length > 0 && !formData} - inputProps={{ autoFocus }} - /> -); diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index ecde218029..6358934025 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -13,10 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './EntityNamePicker'; export * from './EntityPicker'; export * from './OwnerPicker'; export * from './RepoUrlPicker'; -export * from './TextValuePicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; diff --git a/plugins/scaffolder/src/components/hooks/useEventStream.ts b/plugins/scaffolder/src/components/hooks/useEventStream.ts index 7414a35189..8ff9a30d0b 100644 --- a/plugins/scaffolder/src/components/hooks/useEventStream.ts +++ b/plugins/scaffolder/src/components/hooks/useEventStream.ts @@ -15,14 +15,19 @@ */ import { useImmerReducer } from 'use-immer'; import { useEffect } from 'react'; -import { scaffolderApiRef, LogEvent } from '../../api'; -import { ScaffolderTask, Status, TaskOutput } from '../../types'; +import { scaffolderApiRef } from '../../api'; +import { + ScaffolderTask, + ScaffolderTaskStatus, + ScaffolderTaskOutput, + LogEvent, +} from '../../types'; import { useApi } from '@backstage/core-plugin-api'; import { Subscription } from '@backstage/types'; type Step = { id: string; - status: Status; + status: ScaffolderTaskStatus; endedAt?: string; startedAt?: string; }; @@ -34,16 +39,16 @@ export type TaskStream = { completed: boolean; task?: ScaffolderTask; steps: { [stepId in string]: Step }; - output?: TaskOutput; + output?: ScaffolderTaskOutput; }; type ReducerLogEntry = { createdAt: string; body: { stepId?: string; - status?: Status; + status?: ScaffolderTaskStatus; message: string; - output?: TaskOutput; + output?: ScaffolderTaskOutput; }; }; diff --git a/plugins/scaffolder/src/components/index.ts b/plugins/scaffolder/src/components/index.ts new file mode 100644 index 0000000000..47540ff492 --- /dev/null +++ b/plugins/scaffolder/src/components/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './fields'; +export type { RepoUrlPickerUiOptions } from './fields'; +export { TemplateList } from './TemplateList'; +export type { TemplateListProps } from './TemplateList'; +export { TemplateTypePicker } from './TemplateTypePicker'; +export * from './secrets'; +export { TaskPage } from './TaskPage'; +export type { RouterProps } from './Router'; diff --git a/plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx b/plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx index 37d35c9015..3b84c074d7 100644 --- a/plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx +++ b/plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx @@ -36,7 +36,7 @@ describe('SecretsContext', () => { ); expect(result.current.context?.secrets.foo).toEqual(undefined); - act(() => result.current.hook.setSecret({ foo: 'bar' })); + act(() => result.current.hook.setSecrets({ 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 index 6570f9d901..f0b62f20a1 100644 --- a/plugins/scaffolder/src/components/secrets/SecretsContext.tsx +++ b/plugins/scaffolder/src/components/secrets/SecretsContext.tsx @@ -48,11 +48,21 @@ export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => { ); }; +/** + * The return type from the useTemplateSecrets hook. + * @public + */ +export interface ScaffolderUseTemplateSecrets { + /** @deprecated use setSecrets instead */ + setSecret: (input: Record) => void; + setSecrets: (input: Record) => void; +} + /** * Hook to access the secrets context. * @public */ -export const useTemplateSecrets = () => { +export const useTemplateSecrets = (): ScaffolderUseTemplateSecrets => { const value = useContext(SecretsContext); if (!value) { throw new Error( @@ -60,14 +70,14 @@ export const useTemplateSecrets = () => { ); } - const { setSecrets } = value; + const { setSecrets: updateSecrets } = value; - const setSecret = useCallback( + const setSecrets = useCallback( (input: Record) => { - setSecrets(currentSecrets => ({ ...currentSecrets, ...input })); + updateSecrets(currentSecrets => ({ ...currentSecrets, ...input })); }, - [setSecrets], + [updateSecrets], ); - return { setSecret }; + return { setSecret: setSecrets, setSecrets }; }; diff --git a/plugins/scaffolder/src/components/secrets/index.ts b/plugins/scaffolder/src/components/secrets/index.ts index 44adb2a00a..c608834803 100644 --- a/plugins/scaffolder/src/components/secrets/index.ts +++ b/plugins/scaffolder/src/components/secrets/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { useTemplateSecrets } from './SecretsContext'; +export type { ScaffolderUseTemplateSecrets } from './SecretsContext'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 820cc0e6d3..8081dcb0ca 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -13,21 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityPicker } from '../components/fields/EntityPicker'; -import { - EntityNamePicker, - entityNamePickerValidation, -} from '../components/fields/EntityNamePicker'; -import { EntityTagsPicker } from '../components/fields/EntityTagsPicker'; -import { OwnerPicker } from '../components/fields/OwnerPicker'; -import { - repoPickerValidation, - RepoUrlPicker, -} from '../components/fields/RepoUrlPicker'; -import { FieldExtensionOptions } from './types'; -import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker'; +import { EntityPicker } from '../components/fields/EntityPicker/EntityPicker'; +import { EntityNamePicker } from '../components/fields/EntityNamePicker/EntityNamePicker'; +import { entityNamePickerValidation } from '../components/fields/EntityNamePicker/validation'; +import { EntityTagsPicker } from '../components/fields/EntityTagsPicker/EntityTagsPicker'; +import { OwnerPicker } from '../components/fields/OwnerPicker/OwnerPicker'; +import { RepoUrlPicker } from '../components/fields/RepoUrlPicker/RepoUrlPicker'; +import { repoPickerValidation } from '../components/fields/RepoUrlPicker/validation'; +import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; -export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [ +export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { component: EntityPicker, name: 'EntityPicker', diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index f9c167a6b0..60b9022053 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -25,9 +25,24 @@ import { Extension, attachComponentData } from '@backstage/core-plugin-api'; export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1'; -export function createScaffolderFieldExtension( - options: FieldExtensionOptions, -): Extension<() => null> { +/** + * A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps + * + * @public + */ +export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; + +/** + * Method for creating field extensions that can be used in the scaffolder + * frontend form. + * @public + */ +export function createScaffolderFieldExtension< + TReturnValue = unknown, + TInputProps = unknown, +>( + options: FieldExtensionOptions, +): Extension> { return { expose() { const FieldExtensionDataHolder: any = () => null; @@ -43,7 +58,14 @@ export function createScaffolderFieldExtension( }; } -export const ScaffolderFieldExtensions: React.ComponentType = () => null; +/** + * The Wrapping component for defining fields extensions inside + * + * @public + */ +export const ScaffolderFieldExtensions: React.ComponentType = + (): JSX.Element | null => null; + attachComponentData( ScaffolderFieldExtensions, FIELD_EXTENSION_WRAPPER_KEY, diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index e1da9889e7..be8dc14b0a 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -21,13 +21,11 @@ import { FieldValidation, FieldProps } from '@rjsf/core'; * * @public */ -export type CustomFieldValidator = - | ((data: T, field: FieldValidation) => void) - | (( - data: T, - field: FieldValidation, - context: { apiHolder: ApiHolder }, - ) => void); +export type CustomFieldValidator = ( + data: TFieldReturnValue, + field: FieldValidation, + context: { apiHolder: ApiHolder }, +) => void; /** * Type for the Custom Field Extension with the @@ -35,10 +33,15 @@ export type CustomFieldValidator = * * @public */ -export type FieldExtensionOptions = { +export type FieldExtensionOptions< + TFieldReturnValue = unknown, + TInputProps = unknown, +> = { name: string; - component: (props: FieldProps) => JSX.Element | null; - validation?: CustomFieldValidator; + component: ( + props: FieldExtensionComponentProps, + ) => JSX.Element | null; + validation?: CustomFieldValidator; }; /** @@ -48,10 +51,10 @@ export type FieldExtensionOptions = { * @public */ export interface FieldExtensionComponentProps< - ReturnValue, - UiOptions extends {} = {}, -> extends FieldProps { - uiSchema: { - 'ui:options'?: UiOptions; + TFieldReturnValue, + TUiOptions extends {} = {}, +> extends FieldProps { + uiSchema: FieldProps['uiSchema'] & { + 'ui:options'?: TUiOptions; }; } diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index adb2aab035..f33cf289f4 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -21,7 +21,21 @@ */ export { scaffolderApiRef, ScaffolderClient } from './api'; -export type { ScaffolderApi } from './api'; +export type { + JobStatus, + ListActionsResponse, + LogEvent, + ScaffolderApi, + ScaffolderGetIntegrationsListOptions, + ScaffolderGetIntegrationsListResponse, + ScaffolderScaffoldOptions, + ScaffolderScaffoldResponse, + ScaffolderStreamLogsOptions, + ScaffolderTask, + ScaffolderTaskOutput, + ScaffolderTaskStatus, + TemplateParameterSchema, +} from './types'; export { createScaffolderFieldExtension, ScaffolderFieldExtensions, @@ -30,6 +44,7 @@ export type { CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, + FieldExtensionComponent, } from './extensions'; export { EntityPickerFieldExtension, @@ -39,23 +54,7 @@ export { OwnedEntityPickerFieldExtension, RepoUrlPickerFieldExtension, ScaffolderPage, - scaffolderPlugin as plugin, scaffolderPlugin, } from './plugin'; -export { - EntityNamePicker, - EntityPicker, - EntityTagsPicker, - OwnerPicker, - RepoUrlPicker, - TextValuePicker, - OwnedEntityPicker, -} from './components/fields'; -export type { RepoUrlPickerUiOptions } from './components/fields'; -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 * from './components'; export type { TaskPageProps } from './components/TaskPage'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index ccf8d2ee34..0d44a40c80 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -16,16 +16,12 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef, ScaffolderClient } from './api'; -import { EntityPicker } from './components/fields/EntityPicker'; -import { - entityNamePickerValidation, - EntityNamePicker, -} from './components/fields/EntityNamePicker'; -import { OwnerPicker } from './components/fields/OwnerPicker'; -import { - repoPickerValidation, - RepoUrlPicker, -} from './components/fields/RepoUrlPicker'; +import { EntityPicker } from './components/fields/EntityPicker/EntityPicker'; +import { entityNamePickerValidation } from './components/fields/EntityNamePicker'; +import { EntityNamePicker } from './components/fields/EntityNamePicker/EntityNamePicker'; +import { OwnerPicker } from './components/fields/OwnerPicker/OwnerPicker'; +import { repoPickerValidation } from './components/fields/RepoUrlPicker'; +import { RepoUrlPicker } from './components/fields/RepoUrlPicker/RepoUrlPicker'; import { createScaffolderFieldExtension } from './extensions'; import { registerComponentRouteRef, rootRouteRef } from './routes'; import { @@ -35,8 +31,8 @@ import { discoveryApiRef, fetchApiRef, } from '@backstage/core-plugin-api'; -import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker'; -import { EntityTagsPicker } from './components/fields/EntityTagsPicker'; +import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { EntityTagsPicker } from './components/fields/EntityTagsPicker/EntityTagsPicker'; export const scaffolderPlugin = createPlugin({ id: 'scaffolder', diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index df5868c9ea..97f942d8b9 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -16,6 +16,7 @@ import { createExternalRouteRef, createRouteRef, + createSubRouteRef, } from '@backstage/core-plugin-api'; export const registerComponentRouteRef = createExternalRouteRef({ @@ -26,3 +27,21 @@ export const registerComponentRouteRef = createExternalRouteRef({ export const rootRouteRef = createRouteRef({ id: 'scaffolder', }); + +export const selectedTemplateRouteRef = createSubRouteRef({ + id: 'scaffolder/selected-template', + parent: rootRouteRef, + path: '/templates/:templateName', +}); + +export const scaffolderTaskRouteRef = createSubRouteRef({ + id: 'scaffolder/task', + parent: rootRouteRef, + path: '/tasks/:taskId', +}); + +export const actionsRouteRef = createSubRouteRef({ + id: 'scaffolder/actions', + parent: rootRouteRef, + path: '/actions', +}); diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 291f680710..0ffbd601a8 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -13,31 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JSONSchema } from '@backstage/catalog-model'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { JsonObject, JsonValue, Observable } from '@backstage/types'; +import { JSONSchema7 } from 'json-schema'; + +export type ScaffolderTaskStatus = + | 'open' + | 'processing' + | 'failed' + | 'completed' + | 'skipped'; -export type Status = 'open' | 'processing' | 'failed' | 'completed' | 'skipped'; export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; -export type Job = { - id: string; - metadata: { - entity: any; - values: any; - remoteUrl?: string; - catalogInfoUrl?: string; - }; - status: JobStatus; - stages: Stage[]; - error?: Error; -}; - -export type Stage = { - name: string; - log: string[]; - status: JobStatus; - startedAt: string; - endedAt?: string; -}; export type ScaffolderTask = { id: string; @@ -51,24 +38,100 @@ export type ListActionsResponse = Array<{ id: string; description?: string; schema?: { - input?: JSONSchema; - output?: JSONSchema; + input?: JSONSchema7; + output?: JSONSchema7; }; }>; -type OutputLink = { +type ScaffolderOutputLink = { title?: string; icon?: string; url?: string; entityRef?: string; }; -export type TaskOutput = { +export type ScaffolderTaskOutput = { /** @deprecated use the `links` property to link out to relevant resources */ entityRef?: string; /** @deprecated use the `links` property to link out to relevant resources */ remoteUrl?: string; - links?: OutputLink[]; + links?: ScaffolderOutputLink[]; } & { [key: string]: unknown; }; + +export type TemplateParameterSchema = { + title: string; + steps: Array<{ + title: string; + schema: JsonObject; + }>; +}; + +export type LogEvent = { + type: 'log' | 'completion'; + body: { + message: string; + stepId?: string; + status?: ScaffolderTaskStatus; + }; + createdAt: string; + id: string; + taskId: string; +}; + +export interface ScaffolderScaffoldOptions { + templateRef: string; + values: Record; + secrets?: Record; +} + +export interface ScaffolderScaffoldResponse { + taskId: string; +} + +export interface ScaffolderGetIntegrationsListOptions { + allowedHosts: string[]; +} + +export interface ScaffolderGetIntegrationsListResponse { + integrations: { type: string; title: string; host: string }[]; +} + +export interface ScaffolderStreamLogsOptions { + taskId: string; + after?: number; +} +/** + * An API to interact with the scaffolder backend. + * + * @public + */ +export interface ScaffolderApi { + getTemplateParameterSchema( + templateRef: string, + ): Promise; + + /** + * Executes the scaffolding of a component, given a template and its + * parameter values. + * + * @param options - The {@link ScaffolderScaffoldOptions} the scaffolding. + */ + scaffold( + options: ScaffolderScaffoldOptions, + ): Promise; + + getTask(taskId: string): Promise; + + getIntegrationsList( + options: ScaffolderGetIntegrationsListOptions, + ): Promise; + + /** + * Returns a list of all installed actions. + */ + listActions(): Promise; + + streamLogs(options: ScaffolderStreamLogsOptions): Observable; +} diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index 5dc394be6c..b68824932e 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 0.0.10 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/config@0.1.15 + - @backstage/search-common@0.2.4 + +## 0.0.9 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 4c0332e55c: chore(deps-dev): bump `@elastic/elasticsearch-mock` from 0.3.0 to 1.0.0 +- Updated dependencies + - @backstage/config@0.1.14 + - @backstage/search-common@0.2.3 + ## 0.0.8 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index 6dc0256586..d4edcc6b25 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -5,6 +5,8 @@ ```ts /// +import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { Client } from '@elastic/elasticsearch'; import { Config } from '@backstage/config'; import type { ConnectionOptions } from 'tls'; import { IndexableDocument } from '@backstage/search-common'; @@ -114,7 +116,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { indexPrefix, }: ElasticSearchOptions): Promise; // (undocumented) - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; newClient(create: (options: ElasticSearchClientOptions) => T): T; // (undocumented) query(query: SearchQuery): Promise; @@ -127,4 +129,31 @@ export class ElasticSearchSearchEngine implements SearchEngine { // (undocumented) protected translator(query: SearchQuery): ConcreteElasticSearchQuery; } + +// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor(options: ElasticSearchSearchEngineIndexerOptions); + // (undocumented) + finalize(): Promise; + // (undocumented) + index(documents: IndexableDocument[]): Promise; + // (undocumented) + readonly indexName: string; + // (undocumented) + initialize(): Promise; +} + +// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ElasticSearchSearchEngineIndexerOptions = { + type: string; + indexPrefix: string; + indexSeparator: string; + alias: string; + logger: Logger_2; + elasticSearchClient: Client; +}; ``` diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 6b21d0d0ab..0a279d6b4c 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "0.0.8", + "version": "0.0.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,18 +10,22 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin-module" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/config": "^0.1.13", - "@backstage/search-common": "^0.2.2", + "@backstage/config": "^0.1.15", + "@backstage/search-common": "^0.2.4", + "@backstage/plugin-search-backend-node": "^0.4.7", "@elastic/elasticsearch": "7.13.0", "@acuris/aws-es-connection": "^2.2.0", "aws-sdk": "^2.948.0", @@ -30,9 +34,9 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.10.7-next.0", - "@backstage/cli": "^0.13.2-next.0", - "@elastic/elasticsearch-mock": "^0.3.0" + "@backstage/backend-common": "^0.11.0", + "@backstage/cli": "^0.14.1", + "@elastic/elasticsearch-mock": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts index 1566a35b57..467e1b4e77 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts @@ -15,7 +15,8 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Client } from '@elastic/elasticsearch'; +import { ConfigReader } from '@backstage/config'; +import { Client, errors } from '@elastic/elasticsearch'; import Mock from '@elastic/elasticsearch-mock'; import { ConcreteElasticSearchQuery, @@ -23,7 +24,7 @@ import { ElasticSearchSearchEngine, encodePageCursor, } from './ElasticSearchSearchEngine'; -import { ConfigReader } from '@backstage/config'; +import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; class ElasticSearchSearchEngineForTranslatorTests extends ElasticSearchSearchEngine { getTranslator() { @@ -37,6 +38,16 @@ const options = { Connection: mock.getConnection(), }; +const indexerMock = { + on: jest.fn(), + indexName: 'expected-index-name', +}; +jest.mock('./ElasticSearchSearchEngineIndexer', () => ({ + ElasticSearchSearchEngineIndexer: jest + .fn() + .mockImplementation(() => indexerMock), +})); + describe('ElasticSearchSearchEngine', () => { let testSearchEngine: ElasticSearchSearchEngine; let inspectableSearchEngine: ElasticSearchSearchEngineForTranslatorTests; @@ -542,23 +553,67 @@ describe('ElasticSearchSearchEngine', () => { }); }); - describe('index', () => { - it('should index document', async () => { - const indexSpy = jest.spyOn(testSearchEngine, 'index'); - const mockDocuments = [ - { - title: 'testTerm', - text: 'testText', - location: 'test/location', - }, - ]; + describe('indexer', () => { + it('should get indexer', async () => { + const indexer = await testSearchEngine.getIndexer('test-index'); - // call index func and ensure the index func was invoked. - await testSearchEngine.index('test-index', mockDocuments); - expect(indexSpy).toHaveBeenCalled(); - expect(indexSpy).toHaveBeenCalledWith('test-index', [ - { title: 'testTerm', text: 'testText', location: 'test/location' }, - ]); + expect(indexer).toStrictEqual(indexerMock); + expect(ElasticSearchSearchEngineIndexer).toHaveBeenCalledWith( + expect.objectContaining({ + alias: 'test-index__search', + type: 'test-index', + indexPrefix: '', + indexSeparator: '-index__', + elasticSearchClient: client, + }), + ); + expect(indexerMock.on).toHaveBeenCalledWith( + 'error', + expect.any(Function), + ); + }); + + describe('onError', () => { + let errorHandler: Function; + const error = new Error('some error'); + + beforeEach(async () => { + mock.clearAll(); + await testSearchEngine.getIndexer('test-index'); + errorHandler = indexerMock.on.mock.calls[0][1]; + }); + + it('should check for and delete expected index', async () => { + const existsSpy = jest.fn().mockReturnValue('truthy value'); + const deleteSpy = jest.fn().mockReturnValue({}); + mock.add({ method: 'HEAD', path: '/expected-index-name' }, existsSpy); + mock.add({ method: 'DELETE', path: '/expected-index-name' }, deleteSpy); + + await errorHandler(error); + + // Check and delete HTTP requests were made. + expect(existsSpy).toHaveBeenCalled(); + expect(deleteSpy).toHaveBeenCalled(); + }); + + it('should not delete index if none exists', async () => { + // Exists call returns 404 on no index. + const existsSpy = jest.fn().mockReturnValue( + new errors.ResponseError({ + statusCode: 404, + body: { status: 404 }, + } as unknown as any), + ); + const deleteSpy = jest.fn().mockReturnValue({}); + mock.add({ method: 'HEAD', path: '/expected-index-name' }, existsSpy); + mock.add({ method: 'DELETE', path: '/expected-index-name' }, deleteSpy); + + await errorHandler(error); + + // Check request was made, but no delete request was made. + expect(existsSpy).toHaveBeenCalled(); + expect(deleteSpy).not.toHaveBeenCalled(); + }); }); }); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 48caf0d4f1..c16567fee0 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -29,8 +29,8 @@ import { Client } from '@elastic/elasticsearch'; import esb from 'elastic-builder'; import { isEmpty, isNaN as nan, isNumber } from 'lodash'; import { Logger } from 'winston'; - import type { ElasticSearchClientOptions } from './ElasticSearchClientOptions'; +import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; export type { ElasticSearchClientOptions }; @@ -58,12 +58,6 @@ type ElasticSearchResult = { _source: IndexableDocument; }; -function duration(startTimestamp: [number, number]): string { - const delta = process.hrtime(startTimestamp); - const seconds = delta[0] + delta[1] / 1e9; - return `${seconds.toFixed(1)}s`; -} - function isBlank(str: string) { return (isEmpty(str) && !isNumber(str)) || nan(str); } @@ -165,67 +159,37 @@ export class ElasticSearchSearchEngine implements SearchEngine { this.translator = translator; } - async index(type: string, documents: IndexableDocument[]): Promise { - this.logger.info( - `Started indexing ${documents.length} documents for index ${type}`, - ); - const startTimestamp = process.hrtime(); + async getIndexer(type: string) { const alias = this.constructSearchAlias(type); - const index = this.constructIndexName(type, `${Date.now()}`); - try { - const aliases = await this.elasticSearchClient.cat.aliases({ - format: 'json', - name: alias, - }); - const removableIndices = aliases.body.map( - (r: Record) => r.index, - ); + const indexer = new ElasticSearchSearchEngineIndexer({ + type, + indexPrefix: this.indexPrefix, + indexSeparator: this.indexSeparator, + alias, + elasticSearchClient: this.elasticSearchClient, + logger: this.logger, + }); - await this.elasticSearchClient.indices.create({ - index, - }); - const result = await this.elasticSearchClient.helpers.bulk({ - datasource: documents, - onDocument() { - return { - index: { _index: index }, - }; - }, - refreshOnCompletion: index, - }); - - this.logger.info( - `Indexing completed for index ${type} in ${duration(startTimestamp)}`, - result, - ); - await this.elasticSearchClient.indices.updateAliases({ - body: { - actions: [ - { remove: { index: this.constructIndexName(type, '*'), alias } }, - { add: { index, alias } }, - ], - }, - }); - - this.logger.info('Removing stale search indices', removableIndices); - if (removableIndices.length) { - await this.elasticSearchClient.indices.delete({ - index: removableIndices, - }); - } - } catch (e) { + // Attempt cleanup upon failure. + indexer.on('error', async e => { this.logger.error(`Failed to index documents for type ${type}`, e); - const response = await this.elasticSearchClient.indices.exists({ - index, - }); - const indexCreated = response.body; - if (indexCreated) { - this.logger.info(`Removing created index ${index}`); - await this.elasticSearchClient.indices.delete({ - index, + try { + const response = await this.elasticSearchClient.indices.exists({ + index: indexer.indexName, }); + const indexCreated = response.body; + if (indexCreated) { + this.logger.info(`Removing created index ${indexer.indexName}`); + await this.elasticSearchClient.indices.delete({ + index: indexer.indexName, + }); + } + } catch (error) { + this.logger.error(`Unable to clean up elastic index: ${error}`); } - } + }); + + return indexer; } async query(query: SearchQuery): Promise { @@ -268,10 +232,6 @@ export class ElasticSearchSearchEngine implements SearchEngine { private readonly indexSeparator = '-index__'; - private constructIndexName(type: string, postFix: string) { - return `${this.indexPrefix}${type}${this.indexSeparator}${postFix}`; - } - private getTypeFromIndex(index: string) { return index .substring(this.indexPrefix.length) diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..0867887d9a --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts @@ -0,0 +1,211 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { Client } from '@elastic/elasticsearch'; +import Mock from '@elastic/elasticsearch-mock'; +import { range } from 'lodash'; +import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; + +const mock = new Mock(); +const client = new Client({ + node: 'http://localhost:9200', + Connection: mock.getConnection(), +}); + +describe('ElasticSearchSearchEngineIndexer', () => { + let indexer: ElasticSearchSearchEngineIndexer; + let bulkSpy: jest.Mock; + let catSpy: jest.Mock; + let createSpy: jest.Mock; + let aliasesSpy: jest.Mock; + let deleteSpy: jest.Mock; + + beforeEach(() => { + // Instantiate the indexer to be tested. + indexer = new ElasticSearchSearchEngineIndexer({ + type: 'some-type', + indexPrefix: '', + indexSeparator: '-index__', + alias: 'some-type-index__search', + logger: getVoidLogger(), + elasticSearchClient: client, + }); + + // Set up all requisite Elastic mocks. + mock.clearAll(); + bulkSpy = jest.fn().mockReturnValue({ took: 9, errors: false, items: [] }); + mock.add( + { + method: 'POST', + path: '/_bulk', + }, + bulkSpy, + ); + mock.add( + { + method: 'GET', + path: '/:index/_refresh', + }, + jest.fn().mockReturnValue({}), + ); + + catSpy = jest.fn().mockReturnValue([ + { + alias: 'some-type-index__search', + index: 'some-type-index__123tobedeleted', + filter: '-', + 'routing.index': '-', + 'routing.search': '-', + is_write_index: '-', + }, + ]); + mock.add( + { + method: 'GET', + path: '/_cat/aliases/some-type-index__search', + }, + catSpy, + ); + + createSpy = jest.fn().mockReturnValue({ + acknowledged: true, + shards_acknowledged: true, + index: 'single_index', + }); + mock.add( + { + method: 'PUT', + path: '/:index', + }, + createSpy, + ); + + aliasesSpy = jest.fn().mockReturnValue({}); + mock.add( + { + method: 'POST', + path: '*', + }, + aliasesSpy, + ); + + deleteSpy = jest.fn().mockReturnValue({}); + mock.add( + { + method: 'DELETE', + path: '/some-type-index__123tobedeleted', + }, + deleteSpy, + ); + }); + + it('indexes documents', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + }, + { + title: 'Another test', + text: 'Some more text', + location: 'test/location/2', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Older indices should have been queried for. + expect(catSpy).toHaveBeenCalled(); + + // A new index should have been created. + const createdIndex = createSpy.mock.calls[0][0].path.slice(1); + expect(createdIndex).toContain('some-type-index__'); + + // Bulk helper should have been called with documents. + const bulkBody = bulkSpy.mock.calls[0][0].body; + expect(bulkBody[0]).toStrictEqual({ index: { _index: createdIndex } }); + expect(bulkBody[1]).toStrictEqual(documents[0]); + expect(bulkBody[2]).toStrictEqual({ index: { _index: createdIndex } }); + expect(bulkBody[3]).toStrictEqual(documents[1]); + + // Alias should have been rotated. + expect(aliasesSpy).toHaveBeenCalled(); + const aliasActions = aliasesSpy.mock.calls[0][0].body.actions; + expect(aliasActions[0]).toStrictEqual({ + remove: { index: 'some-type-index__*', alias: 'some-type-index__search' }, + }); + expect(aliasActions[1]).toStrictEqual({ + add: { index: createdIndex, alias: 'some-type-index__search' }, + }); + + // Old index should be cleaned up. + expect(deleteSpy).toHaveBeenCalled(); + }); + + it('handles bulk and batching during indexing', async () => { + const documents = range(550).map(i => ({ + title: `Hello World ${i}`, + location: `location-${i}`, + // Generate large document sizes to trigger ES bulk flushing. + text: range(2000).join(', '), + })); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Ensure multiple bulk requests were made. + expect(bulkSpy).toHaveBeenCalledTimes(2); + + // Ensure the first and last documents were included in the payloads. + const docLocations: string[] = [ + ...bulkSpy.mock.calls[0][0].body.map((l: any) => l.location), + ...bulkSpy.mock.calls[1][0].body.map((l: any) => l.location), + ]; + expect(docLocations).toContain('location-0'); + expect(docLocations).toContain('location-549'); + }); + + it('ignores cleanup when no existing indices exist', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + }, + ]; + + // Update initial alias cat to return nothing. + catSpy = jest.fn().mockReturnValue([]); + mock.clear({ + method: 'GET', + path: '/_cat/aliases/some-type-index__search', + }); + mock.add( + { + method: 'GET', + path: '/_cat/aliases/some-type-index__search', + }, + catSpy, + ); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Final deletion shouldn't be called. + expect(deleteSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts new file mode 100644 index 0000000000..2e4996cd2f --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts @@ -0,0 +1,176 @@ +/* + * 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 { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { IndexableDocument } from '@backstage/search-common'; +import { Client } from '@elastic/elasticsearch'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; + +export type ElasticSearchSearchEngineIndexerOptions = { + type: string; + indexPrefix: string; + indexSeparator: string; + alias: string; + logger: Logger; + elasticSearchClient: Client; +}; + +function duration(startTimestamp: [number, number]): string { + const delta = process.hrtime(startTimestamp); + const seconds = delta[0] + delta[1] / 1e9; + return `${seconds.toFixed(1)}s`; +} + +export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { + private received: number = 0; + private processed: number = 0; + private removableIndices: string[] = []; + + private readonly startTimestamp: [number, number]; + private readonly type: string; + public readonly indexName: string; + private readonly indexPrefix: string; + private readonly indexSeparator: string; + private readonly alias: string; + private readonly logger: Logger; + private readonly sourceStream: Readable; + private readonly elasticSearchClient: Client; + private bulkResult: Promise; + + constructor(options: ElasticSearchSearchEngineIndexerOptions) { + super({ batchSize: 100 }); + this.logger = options.logger; + this.startTimestamp = process.hrtime(); + this.type = options.type; + this.indexPrefix = options.indexPrefix; + this.indexSeparator = options.indexSeparator; + this.indexName = this.constructIndexName(`${Date.now()}`); + this.alias = options.alias; + this.elasticSearchClient = options.elasticSearchClient; + + // The ES client bulk helper supports stream-based indexing, but we have to + // supply the stream directly to it at instantiation-time. We can't supply + // this class itself, so instead, we create this inline stream instead. + this.sourceStream = new Readable({ objectMode: true }); + this.sourceStream._read = () => {}; + + // eslint-disable-next-line consistent-this + const that = this; + + // Keep a reference to the ES Bulk helper so that we can know when all + // documents have been successfully written to ES. + this.bulkResult = this.elasticSearchClient.helpers.bulk({ + datasource: this.sourceStream, + onDocument() { + that.processed++; + return { + index: { _index: that.indexName }, + }; + }, + refreshOnCompletion: that.indexName, + }); + } + + async initialize(): Promise { + this.logger.info(`Started indexing documents for index ${this.type}`); + + const aliases = await this.elasticSearchClient.cat.aliases({ + format: 'json', + name: this.alias, + }); + + this.removableIndices = aliases.body.map( + (r: Record) => r.index, + ); + + await this.elasticSearchClient.indices.create({ + index: this.indexName, + }); + } + + async index(documents: IndexableDocument[]): Promise { + await this.isReady(); + documents.forEach(document => { + this.received++; + this.sourceStream.push(document); + }); + } + + async finalize(): Promise { + // Wait for all documents to be processed. + await this.isReady(); + + // Close off the underlying stream connected to ES, indicating that no more + // documents will be written. + this.sourceStream.push(null); + + // Wait for the bulk helper to finish processing. + const result = await this.bulkResult; + + // Rotate aliases upon completion. Allow errors to bubble up so that we can + // clean up the create index. + this.logger.info( + `Indexing completed for index ${this.type} in ${duration( + this.startTimestamp, + )}`, + result, + ); + await this.elasticSearchClient.indices.updateAliases({ + body: { + actions: [ + { + remove: { index: this.constructIndexName('*'), alias: this.alias }, + }, + { add: { index: this.indexName, alias: this.alias } }, + ], + }, + }); + + // If any indices are removable, remove them. Do not bubble up this error, + // as doing so would delete the now aliased index. Log instead. + if (this.removableIndices.length) { + this.logger.info('Removing stale search indices', this.removableIndices); + try { + await this.elasticSearchClient.indices.delete({ + index: this.removableIndices, + }); + } catch (e) { + this.logger.warn(`Failed to remove stale search indices: ${e}`); + } + } + } + + /** + * Ensures that the number of documents sent over the wire to ES matches the + * number of documents this stream has received so far. This helps manage + * backpressure in other parts of the indexing pipeline. + */ + private isReady(): Promise { + return new Promise(resolve => { + const interval = setInterval(() => { + if (this.received === this.processed) { + clearInterval(interval); + resolve(); + } + }, 50); + }); + } + + private constructIndexName(postFix: string) { + return `${this.indexPrefix}${this.type}${this.indexSeparator}${postFix}`; + } +} diff --git a/plugins/search-backend-module-elasticsearch/src/engines/index.ts b/plugins/search-backend-module-elasticsearch/src/engines/index.ts index d5eee37803..19c24b37ff 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/index.ts @@ -19,3 +19,7 @@ export type { ConcreteElasticSearchQuery, ElasticSearchClientOptions, } from './ElasticSearchSearchEngine'; +export type { + ElasticSearchSearchEngineIndexer, + ElasticSearchSearchEngineIndexerOptions, +} from './ElasticSearchSearchEngineIndexer'; diff --git a/plugins/search-backend-module-elasticsearch/src/index.ts b/plugins/search-backend-module-elasticsearch/src/index.ts index 8cf96de858..223141be6b 100644 --- a/plugins/search-backend-module-elasticsearch/src/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/index.ts @@ -21,4 +21,8 @@ */ export { ElasticSearchSearchEngine } from './engines'; -export type { ElasticSearchClientOptions } from './engines'; +export type { + ElasticSearchClientOptions, + ElasticSearchSearchEngineIndexer, + ElasticSearchSearchEngineIndexerOptions, +} from './engines'; diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index c0b1f6778d..08986f6d34 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-search-backend-module-pg +## 0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + +## 0.2.8 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/search-common@0.2.4 + - @backstage/plugin-search-backend-node@0.4.7 + +## 0.2.7 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/search-common@0.2.3 + - @backstage/plugin-search-backend-node@0.4.6 + ## 0.2.6 ### Patch Changes diff --git a/plugins/search-backend-module-pg/api-report.md b/plugins/search-backend-module-pg/api-report.md index e2edfc291a..ba8fde5f17 100644 --- a/plugins/search-backend-module-pg/api-report.md +++ b/plugins/search-backend-module-pg/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; import { IndexableDocument } from '@backstage/search-common'; import { Knex } from 'knex'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -28,6 +29,8 @@ export class DatabaseDocumentStore implements DatabaseStore { // (undocumented) static create(knex: Knex): Promise; // (undocumented) + getTransaction(): Promise; + // (undocumented) insertDocuments( tx: Knex.Transaction, type: string, @@ -55,6 +58,8 @@ export interface DatabaseStore { // (undocumented) completeInsert(tx: Knex.Transaction, type: string): Promise; // (undocumented) + getTransaction(): Promise; + // (undocumented) insertDocuments( tx: Knex.Transaction, type: string, @@ -81,7 +86,7 @@ export class PgSearchEngine implements SearchEngine { database: PluginDatabaseManager; }): Promise; // (undocumented) - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; // (undocumented) query(query: SearchQuery): Promise; // (undocumented) @@ -94,6 +99,28 @@ export class PgSearchEngine implements SearchEngine { translator(query: SearchQuery): ConcretePgSearchQuery; } +// Warning: (ae-missing-release-tag) "PgSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor(options: PgSearchEngineIndexerOptions); + // (undocumented) + finalize(): Promise; + // (undocumented) + index(documents: IndexableDocument[]): Promise; + // (undocumented) + initialize(): Promise; +} + +// Warning: (ae-missing-release-tag) "PgSearchEngineIndexerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PgSearchEngineIndexerOptions = { + batchSize: number; + type: string; + databaseStore: DatabaseStore; +}; + // Warning: (ae-missing-release-tag) "PgSearchQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 392b9af73a..d006c4e675 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.6", + "version": "0.2.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,25 +10,28 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin-module" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/search-common": "^0.2.2", - "@backstage/plugin-search-backend-node": "^0.4.5", + "@backstage/backend-common": "^0.11.0", + "@backstage/search-common": "^0.2.4", + "@backstage/plugin-search-backend-node": "^0.4.7", "lodash": "^4.17.21", "knex": "^1.0.2" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.17", - "@backstage/cli": "^0.13.2" + "@backstage/backend-test-utils": "^0.1.19", + "@backstage/cli": "^0.14.1" }, "files": [ "dist", diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts index 56a3a6e643..8618bed4c8 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { range } from 'lodash'; import { DatabaseStore } from '../database'; import { ConcretePgSearchQuery, @@ -21,6 +20,13 @@ import { encodePageCursor, PgSearchEngine, } from './PgSearchEngine'; +import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; + +jest.mock('./PgSearchEngineIndexer', () => ({ + PgSearchEngineIndexer: jest + .fn() + .mockImplementation(async () => 'the-expected-indexer'), +})); describe('PgSearchEngine', () => { const tx: any = {} as any; @@ -30,6 +36,7 @@ describe('PgSearchEngine', () => { beforeEach(() => { database = { transaction: jest.fn(), + getTransaction: jest.fn(), insertDocuments: jest.fn(), query: jest.fn(), completeInsert: jest.fn(), @@ -122,46 +129,21 @@ describe('PgSearchEngine', () => { }); }); - describe('insert', () => { - it('should insert documents', async () => { - database.transaction.mockImplementation(fn => fn(tx)); + describe('index', () => { + it('should instantiate indexer', async () => { + const indexer = await searchEngine.getIndexer('my-type'); - const documents = [ - { title: 'Hello World', text: 'Lorem Ipsum', location: 'location-1' }, - { - location: 'location-2', - text: 'Hello World', - title: 'Dolor sit amet', - }, - ]; - - await searchEngine.index('my-type', documents); - - expect(database.transaction).toHaveBeenCalledTimes(1); - expect(database.prepareInsert).toHaveBeenCalledTimes(1); - expect(database.insertDocuments).toHaveBeenCalledWith( - tx, - 'my-type', - documents, + // Indexer instantiated with expected args. + expect(PgSearchEngineIndexer).toHaveBeenCalledWith( + expect.objectContaining({ + batchSize: 100, + type: 'my-type', + databaseStore: database, + }), ); - expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); - }); - it('should batch insert documents', async () => { - database.transaction.mockImplementation(fn => fn(tx)); - - const documents = range(350).map(i => ({ - title: `Hello World ${i}`, - text: 'Lorem Ipsum', - location: `location-${i}`, - })); - - await searchEngine.index('my-type', documents); - - expect(database.transaction).toHaveBeenCalledTimes(1); - expect(database.prepareInsert).toHaveBeenCalledTimes(1); - expect(database.insertDocuments).toBeCalledTimes(4); - expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); + // Indexer is as expected. + expect(indexer).toBe('the-expected-indexer'); }); }); diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index 6fd6c571a8..ac42007401 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -15,12 +15,8 @@ */ import { PluginDatabaseManager } from '@backstage/backend-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -import { - IndexableDocument, - SearchQuery, - SearchResultSet, -} from '@backstage/search-common'; -import { chunk } from 'lodash'; +import { SearchQuery, SearchResultSet } from '@backstage/search-common'; +import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; import { DatabaseDocumentStore, DatabaseStore, @@ -77,16 +73,11 @@ export class PgSearchEngine implements SearchEngine { this.translator = translator; } - async index(type: string, documents: IndexableDocument[]): Promise { - await this.databaseStore.transaction(async tx => { - await this.databaseStore.prepareInsert(tx); - - const batchSize = 100; - for (const documentBatch of chunk(documents, batchSize)) { - await this.databaseStore.insertDocuments(tx, type, documentBatch); - } - - await this.databaseStore.completeInsert(tx, type); + async getIndexer(type: string) { + return new PgSearchEngineIndexer({ + batchSize: 100, + type, + databaseStore: this.databaseStore, }); } diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..1fc7e74fc4 --- /dev/null +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts @@ -0,0 +1,150 @@ +/* + * 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 { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { range } from 'lodash'; +import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; +import { DatabaseStore } from '../database'; + +describe('PgSearchEngineIndexer', () => { + const tx = { + rollback: jest.fn(), + commit: jest.fn(), + } as any; + let database: jest.Mocked; + let indexer: PgSearchEngineIndexer; + + beforeEach(() => { + jest.clearAllMocks(); + database = { + transaction: jest.fn().mockImplementation(fn => fn(tx)), + getTransaction: jest.fn().mockReturnValue(tx), + insertDocuments: jest.fn(), + query: jest.fn(), + completeInsert: jest.fn(), + prepareInsert: jest.fn(), + }; + indexer = new PgSearchEngineIndexer({ + batchSize: 100, + type: 'my-type', + databaseStore: database, + }); + }); + + it('should insert documents', async () => { + const documents = [ + { title: 'Hello World', text: 'Lorem Ipsum', location: 'location-1' }, + { + location: 'location-2', + text: 'Hello World', + title: 'Dolor sit amet', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).toHaveBeenCalledWith( + tx, + 'my-type', + documents, + ); + expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); + expect(tx.commit).toHaveBeenCalled(); + }); + + it('should batch insert documents', async () => { + const documents = range(350).map(i => ({ + title: `Hello World ${i}`, + text: 'Lorem Ipsum', + location: `location-${i}`, + })); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).toBeCalledTimes(4); + expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); + }); + + it('should close out stream and bubble up error on prepare', async () => { + const expectedError = new Error('Prepare error'); + const documents = [ + { + title: `Hello World`, + text: 'Lorem Ipsum', + location: `location`, + }, + ]; + + database.prepareInsert.mockRejectedValueOnce(expectedError); + const result = await TestPipeline.withSubject(indexer) + .withDocuments(documents) + .execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).not.toHaveBeenCalled(); + expect(database.completeInsert).not.toHaveBeenCalled(); + expect(result.error).toBe(expectedError); + expect(tx.rollback).toHaveBeenCalledWith(expectedError); + }); + + it('should close tx and bubble up error on insert', async () => { + const expectedError = new Error('Index error'); + const documents = [ + { + title: `Hello World`, + text: 'Lorem Ipsum', + location: `location`, + }, + ]; + + database.insertDocuments.mockRejectedValueOnce(expectedError); + const result = await TestPipeline.withSubject(indexer) + .withDocuments(documents) + .execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.completeInsert).not.toHaveBeenCalled(); + expect(result.error).toBe(expectedError); + expect(tx.rollback).toHaveBeenCalledWith(expectedError); + }); + + it('should close tx and bubble up error on completion', async () => { + const expectedError = new Error('Completion error'); + const documents = [ + { + title: `Hello World`, + text: 'Lorem Ipsum', + location: `location`, + }, + ]; + + database.completeInsert.mockRejectedValueOnce(expectedError); + const result = await TestPipeline.withSubject(indexer) + .withDocuments(documents) + .execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).toHaveBeenCalledTimes(1); + expect(database.completeInsert).toHaveBeenCalledTimes(1); + expect(result.error).toBe(expectedError); + expect(tx.rollback).toHaveBeenCalledWith(expectedError); + }); +}); diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts new file mode 100644 index 0000000000..53d040cb87 --- /dev/null +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts @@ -0,0 +1,74 @@ +/* + * 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 { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { IndexableDocument } from '@backstage/search-common'; +import { Knex } from 'knex'; +import { DatabaseStore } from '../database'; + +export type PgSearchEngineIndexerOptions = { + batchSize: number; + type: string; + databaseStore: DatabaseStore; +}; + +export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { + private store: DatabaseStore; + private type: string; + private tx: Knex.Transaction | undefined; + + constructor(options: PgSearchEngineIndexerOptions) { + super({ batchSize: options.batchSize }); + this.store = options.databaseStore; + this.type = options.type; + } + + async initialize(): Promise { + this.tx = await this.store.getTransaction(); + try { + await this.store.prepareInsert(this.tx); + } catch (e) { + // In case of error, rollback the transaction and re-throw the error so + // that the stream can be closed and destroyed properly. + this.tx.rollback(e); + throw e; + } + } + + async index(documents: IndexableDocument[]): Promise { + try { + await this.store.insertDocuments(this.tx!, this.type, documents); + } catch (e) { + // In case of error, rollback the transaction and re-throw the error so + // that the stream can be closed and destroyed properly. + this.tx!.rollback(e); + throw e; + } + } + + async finalize(): Promise { + // Attempt to complete and commit the transaction. + try { + await this.store.completeInsert(this.tx!, this.type); + this.tx!.commit(); + } catch (e) { + // Otherwise, rollback the transaction and re-throw the error so that the + // stream can be closed and destroyed properly. + this.tx!.rollback!(e); + throw e; + } + } +} diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts index 7994998baf..7f8e297648 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts @@ -15,3 +15,7 @@ */ export { PgSearchEngine } from './PgSearchEngine'; export type { ConcretePgSearchQuery } from './PgSearchEngine'; +export type { + PgSearchEngineIndexer, + PgSearchEngineIndexerOptions, +} from './PgSearchEngineIndexer'; diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts index 8c180ea019..0d3ee63ca4 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts @@ -71,6 +71,10 @@ export class DatabaseDocumentStore implements DatabaseStore { return await this.db.transaction(fn); } + async getTransaction(): Promise { + return this.db.transaction(); + } + async prepareInsert(tx: Knex.Transaction): Promise { // We create a temporary table to collect the hashes of the documents that // we expect to be in the documents table at the end. The table is deleted diff --git a/plugins/search-backend-module-pg/src/database/types.ts b/plugins/search-backend-module-pg/src/database/types.ts index 0c0596160e..0a0dc35682 100644 --- a/plugins/search-backend-module-pg/src/database/types.ts +++ b/plugins/search-backend-module-pg/src/database/types.ts @@ -26,6 +26,7 @@ export interface PgSearchQuery { export interface DatabaseStore { transaction(fn: (tx: Knex.Transaction) => Promise): Promise; + getTransaction(): Promise; prepareInsert(tx: Knex.Transaction): Promise; insertDocuments( tx: Knex.Transaction, diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 6f664c086c..864376a7e7 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-search-backend-node +## 0.4.7 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/search-common@0.2.4 + +## 0.4.6 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/search-common@0.2.3 + ## 0.4.5 ### Patch Changes diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 1929009760..37945c8902 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -3,30 +3,60 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { DocumentCollator } from '@backstage/search-common'; -import { DocumentDecorator } from '@backstage/search-common'; +/// + +import { DocumentCollatorFactory } from '@backstage/search-common'; +import { DocumentDecoratorFactory } from '@backstage/search-common'; import { DocumentTypeInfo } from '@backstage/search-common'; import { IndexableDocument } from '@backstage/search-common'; import { Logger as Logger_2 } from 'winston'; import { default as lunr_2 } from 'lunr'; import { QueryTranslator } from '@backstage/search-common'; +import { Readable } from 'stream'; import { SearchEngine } from '@backstage/search-common'; import { SearchQuery } from '@backstage/search-common'; import { SearchResultSet } from '@backstage/search-common'; +import { Transform } from 'stream'; +import { Writable } from 'stream'; -// Warning: (ae-missing-release-tag) "IndexBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta +export abstract class BatchSearchEngineIndexer extends Writable { + constructor(options: BatchSearchEngineOptions); + abstract finalize(): Promise; + abstract index(documents: IndexableDocument[]): Promise; + abstract initialize(): Promise; +} + +// @beta (undocumented) +export type BatchSearchEngineOptions = { + batchSize: number; +}; + +// @beta (undocumented) +export type ConcreteLunrQuery = { + lunrQueryBuilder: lunr_2.Index.QueryBuilder; + documentTypes?: string[]; + pageSize: number; +}; + +// @beta +export abstract class DecoratorBase extends Transform { + constructor(); + abstract decorate( + document: IndexableDocument, + ): Promise; + abstract finalize(): Promise; + abstract initialize(): Promise; +} + +// @beta (undocumented) export class IndexBuilder { - // Warning: (ae-forgotten-export) The symbol "IndexBuilderOptions" needs to be exported by the entry point index.d.ts constructor({ logger, searchEngine }: IndexBuilderOptions); - // Warning: (ae-forgotten-export) The symbol "RegisterCollatorParameters" needs to be exported by the entry point index.d.ts addCollator({ - collator, + factory, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void; - // Warning: (ae-forgotten-export) The symbol "RegisterDecoratorParameters" needs to be exported by the entry point index.d.ts - addDecorator({ decorator }: RegisterDecoratorParameters): void; + addDecorator({ factory }: RegisterDecoratorParameters): void; build(): Promise<{ scheduler: Scheduler; }>; @@ -36,32 +66,61 @@ export class IndexBuilder { getSearchEngine(): SearchEngine; } -// Warning: (ae-missing-release-tag) "LunrSearchEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) +export type IndexBuilderOptions = { + searchEngine: SearchEngine; + logger: Logger_2; +}; + +// @beta (undocumented) +export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; + +// @beta (undocumented) export class LunrSearchEngine implements SearchEngine { constructor({ logger }: { logger: Logger_2 }); // (undocumented) protected docStore: Record; // (undocumented) - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; // (undocumented) protected logger: Logger_2; // (undocumented) protected lunrIndices: Record; // (undocumented) query(query: SearchQuery): Promise; - // Warning: (ae-forgotten-export) The symbol "LunrQueryTranslator" needs to be exported by the entry point index.d.ts - // // (undocumented) setTranslator(translator: LunrQueryTranslator): void; // (undocumented) protected translator: QueryTranslator; } -// Warning: (ae-missing-release-tag) "Scheduler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta (undocumented) +export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor(); + // (undocumented) + buildIndex(): lunr_2.Index; + // (undocumented) + finalize(): Promise; + // (undocumented) + getDocumentStore(): Record; + // (undocumented) + index(documents: IndexableDocument[]): Promise; + // (undocumented) + initialize(): Promise; +} + +// @beta +export interface RegisterCollatorParameters { + defaultRefreshIntervalSeconds: number; + factory: DocumentCollatorFactory; +} + +// @beta +export interface RegisterDecoratorParameters { + factory: DocumentDecoratorFactory; +} + +// @beta (undocumented) export class Scheduler { constructor({ logger }: { logger: Logger_2 }); addToSchedule(task: Function, interval: number): void; @@ -70,4 +129,17 @@ export class Scheduler { } export { SearchEngine }; + +// @beta +export class TestPipeline { + execute(): Promise; + withDocuments(documents: IndexableDocument[]): TestPipeline; + static withSubject(subject: Readable | Transform | Writable): TestPipeline; +} + +// @beta +export type TestPipelineResult = { + error: unknown; + documents: IndexableDocument[]; +}; ``` diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index e2d83d0f4f..2c644e357d 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "0.4.5", + "version": "0.4.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,24 +10,29 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "node-library" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/search-common": "^0.2.2", - "winston": "^3.2.1", + "@backstage/errors": "^0.2.2", + "@backstage/search-common": "^0.2.4", + "@types/lunr": "^2.3.3", + "lodash": "^4.17.21", "lunr": "^2.3.9", - "@types/lunr": "^2.3.3" + "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.10.7-next.0", - "@backstage/cli": "^0.13.2-next.0" + "@backstage/backend-common": "^0.11.0", + "@backstage/cli": "^0.14.1" }, "files": [ "dist" diff --git a/plugins/search-backend-node/src/IndexBuilder.test.ts b/plugins/search-backend-node/src/IndexBuilder.test.ts index 0465f2701c..95a2e1d439 100644 --- a/plugins/search-backend-node/src/IndexBuilder.test.ts +++ b/plugins/search-backend-node/src/IndexBuilder.test.ts @@ -16,35 +16,37 @@ import { getVoidLogger } from '@backstage/backend-common'; import { - DocumentCollator, - DocumentDecorator, - IndexableDocument, + DocumentCollatorFactory, + DocumentDecoratorFactory, } from '@backstage/search-common'; +import { Readable, Transform } from 'stream'; import { IndexBuilder } from './IndexBuilder'; import { LunrSearchEngine, SearchEngine } from './index'; -class TestDocumentCollator implements DocumentCollator { +class TestDocumentCollatorFactory implements DocumentCollatorFactory { readonly type: string = 'anything'; - async execute(): Promise { - return []; + async getCollator(): Promise { + const collator = new Readable({ objectMode: true }); + collator._read = () => {}; + return collator; } } -class TypedDocumentCollator extends TestDocumentCollator { +class TypedDocumentCollatorFactory extends TestDocumentCollatorFactory { readonly type = 'an-expected-type'; } -class TestDocumentDecorator implements DocumentDecorator { - async execute(documents: IndexableDocument[]) { - return documents; +class TestDocumentDecoratorFactory implements DocumentDecoratorFactory { + async getDecorator(): Promise { + return new Transform(); } } -class TypedDocumentDecorator extends TestDocumentDecorator { +class TypedDocumentDecoratorFactory extends TestDocumentDecoratorFactory { readonly types = ['an-expected-type']; } -class DifferentlyTypedDocumentDecorator extends TestDocumentDecorator { +class DifferentlyTypedDocumentDecoratorFactory extends TestDocumentDecoratorFactory { readonly types = ['not-the-expected-type']; } @@ -64,13 +66,13 @@ describe('IndexBuilder', () => { describe('addCollator', () => { it('adds a collator', async () => { jest.useFakeTimers(); - const testCollator = new TestDocumentCollator(); - const collatorSpy = jest.spyOn(testCollator, 'execute'); + const testCollatorFactory = new TestDocumentCollatorFactory(); + const collatorSpy = jest.spyOn(testCollatorFactory, 'getCollator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Build the index and ensure the collator was invoked. @@ -84,19 +86,19 @@ describe('IndexBuilder', () => { describe('addDecorator', () => { it('adds a decorator', async () => { jest.useFakeTimers(); - const testCollator = new TestDocumentCollator(); - const testDecorator = new TestDocumentDecorator(); - const decoratorSpy = jest.spyOn(testDecorator, 'execute'); + const testCollatorFactory = new TestDocumentCollatorFactory(); + const testDecoratorFactory = new TestDocumentDecoratorFactory(); + const decoratorSpy = jest.spyOn(testDecoratorFactory, 'getDecorator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Add a decorator. testIndexBuilder.addDecorator({ - decorator: testDecorator, + factory: testDecoratorFactory, }); // Build the index and ensure the decorator was invoked. @@ -110,27 +112,20 @@ describe('IndexBuilder', () => { it('adds a type-specific decorator', async () => { jest.useFakeTimers(); - const testCollator = new TypedDocumentCollator(); - const testDecorator = new TypedDocumentDecorator(); - const docFixture = { - title: 'Test', - text: 'Test text.', - location: '/test/location', - }; - jest - .spyOn(testCollator, 'execute') - .mockImplementation(async () => [docFixture]); - const decoratorSpy = jest.spyOn(testDecorator, 'execute'); + const testCollatorFactory = new TypedDocumentCollatorFactory(); + const testDecoratorFactory = new TypedDocumentDecoratorFactory(); + jest.spyOn(testCollatorFactory, 'getCollator'); + const decoratorSpy = jest.spyOn(testDecoratorFactory, 'getDecorator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Add a decorator for the same type. testIndexBuilder.addDecorator({ - decorator: testDecorator, + factory: testDecoratorFactory, }); // Build the index and ensure the decorator was invoked. @@ -140,31 +135,24 @@ describe('IndexBuilder', () => { // wait for async decorator execution await Promise.resolve(); expect(decoratorSpy).toHaveBeenCalled(); - expect(decoratorSpy).toHaveBeenCalledWith([docFixture]); }); it('adds a type-specific decorator that should not be called', async () => { - const docFixture = { - title: 'Test', - text: 'Test text.', - location: '/test/location', - }; - const testCollator = new TestDocumentCollator(); - const testDecorator = new DifferentlyTypedDocumentDecorator(); - const collatorSpy = jest - .spyOn(testCollator, 'execute') - .mockImplementation(async () => [docFixture]); - const decoratorSpy = jest.spyOn(testDecorator, 'execute'); + const testCollatorFactory = new TestDocumentCollatorFactory(); + const testDecoratorFactory = + new DifferentlyTypedDocumentDecoratorFactory(); + const collatorSpy = jest.spyOn(testCollatorFactory, 'getCollator'); + const decoratorSpy = jest.spyOn(testDecoratorFactory, 'getDecorator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Add a decorator for a different type. testIndexBuilder.addDecorator({ - decorator: testDecorator, + factory: testDecoratorFactory, }); // Build the index and ensure the decorator was not invoked. diff --git a/plugins/search-backend-node/src/IndexBuilder.ts b/plugins/search-backend-node/src/IndexBuilder.ts index 92adb03d7d..e0a059e303 100644 --- a/plugins/search-backend-node/src/IndexBuilder.ts +++ b/plugins/search-backend-node/src/IndexBuilder.ts @@ -15,32 +15,31 @@ */ import { - DocumentCollator, - DocumentDecorator, + DocumentCollatorFactory, + DocumentDecoratorFactory, DocumentTypeInfo, - IndexableDocument, SearchEngine, } from '@backstage/search-common'; +import { Transform, pipeline } from 'stream'; import { Logger } from 'winston'; import { Scheduler } from './index'; import { + IndexBuilderOptions, RegisterCollatorParameters, RegisterDecoratorParameters, } from './types'; interface CollatorEnvelope { - collate: DocumentCollator; + factory: DocumentCollatorFactory; refreshInterval: number; } -type IndexBuilderOptions = { - searchEngine: SearchEngine; - logger: Logger; -}; - +/** + * @beta + */ export class IndexBuilder { private collators: Record; - private decorators: Record; + private decorators: Record; private documentTypes: Record; private searchEngine: SearchEngine; private logger: Logger; @@ -66,18 +65,18 @@ export class IndexBuilder { * given refresh interval. */ addCollator({ - collator, + factory, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void { this.logger.info( - `Added ${collator.constructor.name} collator for type ${collator.type}`, + `Added ${factory.constructor.name} collator factory for type ${factory.type}`, ); - this.collators[collator.type] = { + this.collators[factory.type] = { refreshInterval: defaultRefreshIntervalSeconds, - collate: collator, + factory, }; - this.documentTypes[collator.type] = { - visibilityPermission: collator.visibilityPermission, + this.documentTypes[factory.type] = { + visibilityPermission: factory.visibilityPermission, }; } @@ -86,18 +85,18 @@ export class IndexBuilder { * the decorator, it will be applied to documents from all known collators, * otherwise it will only be applied to documents of the given types. */ - addDecorator({ decorator }: RegisterDecoratorParameters): void { - const types = decorator.types || ['*']; + addDecorator({ factory }: RegisterDecoratorParameters): void { + const types = factory.types || ['*']; this.logger.info( - `Added decorator ${decorator.constructor.name} to types ${types.join( + `Added decorator ${factory.constructor.name} to types ${types.join( ', ', )}`, ); types.forEach(type => { if (this.decorators.hasOwnProperty(type)) { - this.decorators[type].push(decorator); + this.decorators[type].push(factory); } else { - this.decorators[type] = [decorator]; + this.decorators[type] = [factory]; } }); } @@ -111,46 +110,43 @@ export class IndexBuilder { Object.keys(this.collators).forEach(type => { scheduler.addToSchedule(async () => { - // Collate, Decorate, Index. - const decorators: DocumentDecorator[] = ( - this.decorators['*'] || [] - ).concat(this.decorators[type] || []); - - this.logger.debug( - `Collating documents for ${type} via ${this.collators[type].collate.constructor.name}`, + // Instantiate the collator. + const collator = await this.collators[type].factory.getCollator(); + this.logger.info( + `Collating documents for ${type} via ${this.collators[type].factory.constructor.name}`, ); - let documents: IndexableDocument[]; - try { - documents = await this.collators[type].collate.execute(); - } catch (e) { - this.logger.error( - `Collating documents for ${type} via ${this.collators[type].collate.constructor.name} failed: ${e}`, - ); - return; - } + // Instantiate all relevant decorators. + const decorators: Transform[] = await Promise.all( + (this.decorators['*'] || []) + .concat(this.decorators[type] || []) + .map(async factory => { + const decorator = await factory.getDecorator(); + this.logger.info( + `Attached decorator via ${factory.constructor.name} to ${type} index pipeline.`, + ); + return decorator; + }), + ); - for (let i = 0; i < decorators.length; i++) { - this.logger.debug( - `Decorating ${type} documents via ${decorators[i].constructor.name}`, - ); - try { - documents = await decorators[i].execute(documents); - } catch (e) { - this.logger.error( - `Decorating ${type} documents via ${decorators[i].constructor.name} failed: ${e}`, - ); - return; - } - } + // Instantiate the indexer. + const indexer = await this.searchEngine.getIndexer(type); - if (!documents || documents.length === 0) { - this.logger.debug(`No documents for type "${type}" to index`); - return; - } + // Compose collator/decorators/indexer into a pipeline + return new Promise(done => { + pipeline([collator, ...decorators, indexer], error => { + if (error) { + this.logger.error( + `Collating documents for ${type} failed: ${error}`, + ); + } else { + this.logger.info(`Collating documents for ${type} succeeded`); + } - // pushing documents to index to a configured search engine. - await this.searchEngine.index(type, documents); + // Signal index pipeline completion! + done(); + }); + }); }, this.collators[type].refreshInterval * 1000); }); diff --git a/plugins/search-backend-node/src/Scheduler.ts b/plugins/search-backend-node/src/Scheduler.ts index 3e356aa6aa..6debaa3dcd 100644 --- a/plugins/search-backend-node/src/Scheduler.ts +++ b/plugins/search-backend-node/src/Scheduler.ts @@ -26,6 +26,9 @@ type TaskEnvelope = { * TODO: coordination, error handling */ +/** + * @beta + */ export class Scheduler { private logger: Logger; private schedule: TaskEnvelope[]; diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index cbe48ef5a7..88a3ba9a24 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -16,28 +16,61 @@ import { getVoidLogger } from '@backstage/backend-common'; import lunr from 'lunr'; -import { SearchEngine } from '@backstage/search-common'; +import { IndexableDocument, SearchEngine } from '@backstage/search-common'; import { ConcreteLunrQuery, LunrSearchEngine, decodePageCursor, encodePageCursor, } from './LunrSearchEngine'; +import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; +import { TestPipeline } from '../test-utils'; /** * Just used to test the default translator shipped with LunrSearchEngine. */ -class LunrSearchEngineForTranslatorTests extends LunrSearchEngine { +class LunrSearchEngineForTests extends LunrSearchEngine { + getDocStore() { + return this.docStore; + } + setDocStore(docStore: Record) { + this.docStore = docStore; + } + getLunrIndices() { + return this.lunrIndices; + } getTranslator() { return this.translator; } } +const indexerMock = { + on: jest.fn(), + buildIndex: jest.fn(), + getDocumentStore: jest.fn(), +}; +jest.mock('./LunrSearchEngineIndexer', () => ({ + LunrSearchEngineIndexer: jest.fn().mockImplementation(() => indexerMock), +})); + +const getActualIndexer = (engine: SearchEngine, index: string) => { + (LunrSearchEngineIndexer as unknown as jest.Mock).mockImplementationOnce( + () => { + const ActualIndexer = jest.requireActual( + './LunrSearchEngineIndexer', + ).LunrSearchEngineIndexer; + return new ActualIndexer(); + }, + ); + return engine.getIndexer(index); +}; + describe('LunrSearchEngine', () => { let testLunrSearchEngine: SearchEngine; beforeEach(() => { testLunrSearchEngine = new LunrSearchEngine({ logger: getVoidLogger() }); + jest.clearAllMocks(); }); describe('translator', () => { @@ -65,7 +98,7 @@ describe('LunrSearchEngine', () => { }); it('should return translated query', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -107,7 +140,7 @@ describe('LunrSearchEngine', () => { }); it('should have default offset and limit', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -148,7 +181,7 @@ describe('LunrSearchEngine', () => { }); it('should return translated query with 1 filter', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -193,7 +226,7 @@ describe('LunrSearchEngine', () => { }); it('should handle single-item array filter as scalar value', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -224,7 +257,7 @@ describe('LunrSearchEngine', () => { }); it('should return translated query with multiple filters', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -273,7 +306,7 @@ describe('LunrSearchEngine', () => { }); it('should throw if translated query references missing field', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -334,7 +367,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -359,7 +398,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -392,7 +437,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -424,7 +475,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -456,7 +513,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -489,7 +552,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -522,7 +591,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -560,7 +635,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 2 documents - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -604,8 +685,21 @@ describe('LunrSearchEngine', () => { ]; // Mock 2 indices with 1 document each - await testLunrSearchEngine.index('test-index', mockDocuments); - await testLunrSearchEngine.index('test-index-2', mockDocuments2); + const indexer1 = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + const indexer2 = await getActualIndexer( + testLunrSearchEngine, + 'test-index-2', + ); + await TestPipeline.withSubject(indexer1) + .withDocuments(mockDocuments) + .execute(); + await TestPipeline.withSubject(indexer2) + .withDocuments(mockDocuments2) + .execute(); + // Perform search query scoped to "test-index-2" with a filter on the field "extraField" const mockedSearchResult = await testLunrSearchEngine.query({ term: 'testTitle', @@ -642,7 +736,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 2 documents - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -695,8 +795,20 @@ describe('LunrSearchEngine', () => { ]; // Mock 2 indices with 2 documents each - await testLunrSearchEngine.index('test-index', mockDocuments); - await testLunrSearchEngine.index('test-index-2', mockDocuments2); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); + const indexer2 = await getActualIndexer( + testLunrSearchEngine, + 'test-index-2', + ); + await TestPipeline.withSubject(indexer2) + .withDocuments(mockDocuments2) + .execute(); // Perform search query scoped to "test-index-2" const mockedSearchResult = await testLunrSearchEngine.query({ @@ -734,7 +846,13 @@ describe('LunrSearchEngine', () => { location: `test/location/${i}`, })); - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); const mockedSearchResult = await testLunrSearchEngine.query({ term: 'testTitle', @@ -767,7 +885,10 @@ describe('LunrSearchEngine', () => { location: `test/location/${i}`, })); - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer(testLunrSearchEngine, 'test-index'); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); const mockedSearchResult = await testLunrSearchEngine.query({ term: 'testTitle', @@ -793,22 +914,46 @@ describe('LunrSearchEngine', () => { }); describe('index', () => { - it('should index document', async () => { - const indexSpy = jest.spyOn(testLunrSearchEngine, 'index'); - const mockDocuments = [ - { - title: 'testTerm', - text: 'testText', - location: 'test/location', - }, - ]; + it('should get indexer', async () => { + const indexer = await testLunrSearchEngine.getIndexer('test-index'); + expect(LunrSearchEngineIndexer).toHaveBeenCalled(); + expect(indexer.on).toHaveBeenCalledWith('close', expect.any(Function)); + }); - // call index func and ensure the index func was invoked. - await testLunrSearchEngine.index('test-index', mockDocuments); - expect(indexSpy).toHaveBeenCalled(); - expect(indexSpy).toHaveBeenCalledWith('test-index', [ - { title: 'testTerm', text: 'testText', location: 'test/location' }, - ]); + it('should manage indices and docs on close', async () => { + const doc = { title: 'A doc', text: 'test', location: 'some-location' }; + + // Set up an inspectable search engine to pre-set some data. + const inspectableSearchEngine = new LunrSearchEngineForTests({ + logger: getVoidLogger(), + }); + inspectableSearchEngine.setDocStore({ 'existing-location': doc }); + + // Mock methds called by close handler. + indexerMock.buildIndex.mockReturnValueOnce('expected-index'); + indexerMock.getDocumentStore.mockReturnValueOnce({ + 'new-location': doc, + }); + + // Get the indexer and invoke its close handler. + await inspectableSearchEngine.getIndexer('test-index'); + const onClose = indexerMock.on.mock.calls[0][1] as Function; + onClose(); + + // Ensure mocked methods were called. + expect(indexerMock.buildIndex).toHaveBeenCalled(); + expect(indexerMock.getDocumentStore).toHaveBeenCalled(); + + // Ensure the lunr index was written to the search engine. + expect(inspectableSearchEngine.getLunrIndices()).toStrictEqual({ + 'test-index': 'expected-index', + }); + + // Ensure documents are merged into the existing store. + expect(inspectableSearchEngine.getDocStore()).toStrictEqual({ + 'existing-location': doc, + 'new-location': doc, + }); }); }); }); diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index ea51ffe8e1..394cfffac8 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -23,7 +23,11 @@ import { } from '@backstage/search-common'; import lunr from 'lunr'; import { Logger } from 'winston'; +import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; +/** + * @beta + */ export type ConcreteLunrQuery = { lunrQueryBuilder: lunr.Index.QueryBuilder; documentTypes?: string[]; @@ -35,8 +39,14 @@ type LunrResultEnvelope = { type: string; }; -type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; +/** + * @beta + */ +export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; +/** + * @beta + */ export class LunrSearchEngine implements SearchEngine { protected lunrIndices: Record = {}; protected docStore: Record; @@ -124,30 +134,17 @@ export class LunrSearchEngine implements SearchEngine { this.translator = translator; } - async index(type: string, documents: IndexableDocument[]): Promise { - const lunrBuilder = new lunr.Builder(); + async getIndexer(type: string) { + const indexer = new LunrSearchEngineIndexer(); - lunrBuilder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer); - lunrBuilder.searchPipeline.add(lunr.stemmer); - - // Make this lunr index aware of all relevant fields. - Object.keys(documents[0]).forEach(field => { - lunrBuilder.field(field); + indexer.on('close', () => { + // Once the stream is closed, build the index and store the documents in + // memory for later retrieval. + this.lunrIndices[type] = indexer.buildIndex(); + this.docStore = { ...this.docStore, ...indexer.getDocumentStore() }; }); - // Set "location" field as reference field - lunrBuilder.ref('location'); - - documents.forEach((document: IndexableDocument) => { - // Add document to Lunar index - lunrBuilder.add(document); - // Store documents in memory to be able to look up document using the ref during query time - // This is not how you should implement your SearchEngine implementation! Do not copy! - this.docStore[document.location] = document; - }); - - // "Rotate" the index by simply overwriting any existing index of the same name. - this.lunrIndices[type] = lunrBuilder.build(); + return indexer; } async query(query: SearchQuery): Promise { diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..fbb6b153d0 --- /dev/null +++ b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts @@ -0,0 +1,109 @@ +/* + * 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 lunr from 'lunr'; +import { range } from 'lodash'; +import { TestPipeline } from '../test-utils'; +import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; + +const lunrBuilderAddSpy = jest.fn(); +const lunrBuilderRefSpy = jest.fn(); +const lunrBuilderFieldSpy = jest.fn(); +const lunrBuilderPipelineAddSpy = jest.fn(); +const lunrBuilderSearchPipelineAddSpy = jest.fn(); + +jest.mock('lunr', () => { + const actualLunr = jest.requireActual('lunr'); + return { + ...actualLunr, + Builder: jest.fn().mockImplementation(() => { + const actualBuilder = new actualLunr.Builder(); + actualBuilder.add = lunrBuilderAddSpy; + actualBuilder.ref = lunrBuilderRefSpy; + actualBuilder.field = lunrBuilderFieldSpy; + actualBuilder.pipeline.add = lunrBuilderPipelineAddSpy; + actualBuilder.searchPipeline.add = lunrBuilderSearchPipelineAddSpy; + return actualBuilder; + }), + }; +}); + +describe('LunrSearchEngineIndexer', () => { + let indexer: LunrSearchEngineIndexer; + + beforeEach(() => { + jest.clearAllMocks(); + indexer = new LunrSearchEngineIndexer(); + }); + + it('should index documents', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + expect(lunrBuilderAddSpy).toHaveBeenCalledWith(documents[0]); + }); + + it('should index documents in bulk', async () => { + const documents = range(350).map(i => ({ + title: `Hello World ${i}`, + text: 'Lorem Ipsum', + location: `location-${i}`, + })); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + expect(lunrBuilderAddSpy).toHaveBeenCalledTimes(350); + }); + + it('should initialize schema', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + extra: 'field', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Builder ref should be set to location (and only once). + expect(lunrBuilderRefSpy).toHaveBeenCalledTimes(1); + expect(lunrBuilderRefSpy).toHaveBeenLastCalledWith('location'); + + // Builder fields should be based on document fields. + expect(lunrBuilderFieldSpy).toHaveBeenCalledTimes(4); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('title'); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('text'); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('location'); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('extra'); + }); + + it('should configure lunr pipeline', async () => { + expect(lunrBuilderSearchPipelineAddSpy).toHaveBeenLastCalledWith( + lunr.stemmer, + ); + expect(lunrBuilderPipelineAddSpy).toHaveBeenCalledWith( + ...[lunr.trimmer, lunr.stopWordFilter, lunr.stemmer], + ); + }); +}); diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts new file mode 100644 index 0000000000..01c95f07d4 --- /dev/null +++ b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts @@ -0,0 +1,71 @@ +/* + * 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 { IndexableDocument } from '@backstage/search-common'; +import lunr from 'lunr'; +import { BatchSearchEngineIndexer } from '../indexing'; + +/** + * @beta + */ +export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { + private schemaInitialized = false; + private builder: lunr.Builder; + private docStore: Record = {}; + + constructor() { + super({ batchSize: 100 }); + + this.builder = new lunr.Builder(); + this.builder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer); + this.builder.searchPipeline.add(lunr.stemmer); + } + + // No async initialization required. + async initialize(): Promise {} + async finalize(): Promise {} + + async index(documents: IndexableDocument[]): Promise { + if (!this.schemaInitialized) { + // Make this lunr index aware of all relevant fields. + Object.keys(documents[0]).forEach(field => { + this.builder.field(field); + }); + + // Set "location" field as reference field + this.builder.ref('location'); + + this.schemaInitialized = true; + } + + documents.forEach(document => { + // Add document to Lunar index + this.builder.add(document); + + // Store documents in memory to be able to look up document using the ref during query time + // This is not how you should implement your SearchEngine implementation! Do not copy! + this.docStore[document.location] = document; + }); + } + + buildIndex() { + return this.builder.build(); + } + + getDocumentStore() { + return this.docStore; + } +} diff --git a/plugins/search-backend-node/src/engines/index.ts b/plugins/search-backend-node/src/engines/index.ts index 7e6fb86bd4..0d710eadc2 100644 --- a/plugins/search-backend-node/src/engines/index.ts +++ b/plugins/search-backend-node/src/engines/index.ts @@ -15,4 +15,8 @@ */ export { LunrSearchEngine } from './LunrSearchEngine'; -export type { ConcreteLunrQuery } from './LunrSearchEngine'; +export type { + ConcreteLunrQuery, + LunrQueryTranslator, +} from './LunrSearchEngine'; +export type { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; diff --git a/plugins/search-backend-node/src/index.ts b/plugins/search-backend-node/src/index.ts index d49ef195f2..2edab1b01d 100644 --- a/plugins/search-backend-node/src/index.ts +++ b/plugins/search-backend-node/src/index.ts @@ -23,6 +23,18 @@ export { IndexBuilder } from './IndexBuilder'; export { Scheduler } from './Scheduler'; export { LunrSearchEngine } from './engines'; +export type { + ConcreteLunrQuery, + LunrQueryTranslator, + LunrSearchEngineIndexer, +} from './engines'; +export type { + IndexBuilderOptions, + RegisterCollatorParameters, + RegisterDecoratorParameters, +} from './types'; +export * from './indexing'; +export * from './test-utils'; /** * @deprecated Import from @backstage/search-common instead diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..b692ce3aff --- /dev/null +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts @@ -0,0 +1,156 @@ +/* + * 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 { IndexableDocument } from '@backstage/search-common'; +import { BatchSearchEngineIndexer } from './BatchSearchEngineIndexer'; +import { TestPipeline } from '../test-utils'; + +const indexSpy = jest.fn().mockResolvedValue(undefined); +const initializeSpy = jest.fn().mockResolvedValue(undefined); +const finalizeSpy = jest.fn().mockResolvedValue(undefined); + +class ConcreteBatchIndexer extends BatchSearchEngineIndexer { + async index(documents: IndexableDocument[]): Promise { + return indexSpy(documents); + } + async initialize(): Promise { + return initializeSpy(); + } + async finalize(): Promise { + return finalizeSpy(); + } +} + +describe('BatchSearchEngineIndexer', () => { + const document = { + title: 'Some Document', + text: 'Some document text.', + location: '/some/location', + }; + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should work end-to-end', async () => { + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + await TestPipeline.withSubject(indexer) + .withDocuments([document, document, document]) + .execute(); + expect(indexSpy).toHaveBeenCalledTimes(3); + }); + + it('should call initialize at construction', () => { + // @ts-expect-error + const _indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Allow initialization to complete. + setImmediate(() => { + expect(initializeSpy).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('should emit error if initialization throws', () => { + // Cause the initializer to throw. + const expectedError = new Error('some error'); + initializeSpy.mockRejectedValue(expectedError); + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + indexer.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + // Write a document to force the error state to become known. + indexer.write(document); + }); + }); + + it('should call index according to batchSize', () => { + const indexer = new ConcreteBatchIndexer({ batchSize: 2 }); + + return new Promise(done => { + // Listen for it to finish and assert the batches. + indexer.on('finish', () => { + expect(indexSpy).toHaveBeenCalledTimes(2); + expect(indexSpy).toHaveBeenNthCalledWith(1, [document, document]); + expect(indexSpy).toHaveBeenNthCalledWith(2, [document]); + done(); + }); + + // Write batchSize + 1 documents and end the stream. + indexer.write(document); + indexer.write(document); + indexer.write(document); + indexer.end(); + }); + }); + + it('should call index without exceeding batchSize', () => { + const indexer = new ConcreteBatchIndexer({ batchSize: 2 }); + + return new Promise(done => { + // Listen for it to finish and assert that it still wrote. + indexer.on('finish', () => { + expect(indexSpy).toHaveBeenCalledTimes(1); + expect(indexSpy).toHaveBeenNthCalledWith(1, [document]); + done(); + }); + + // Write batchSize - 1 documents and end the stream. + indexer.write(document); + indexer.end(); + }); + }); + + it('should emit error if index throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('index error'); + indexSpy.mockRejectedValue(expectedError); + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + indexer.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + indexer.write(document); + }); + }); + + it('should emit error if finalize throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('finalize error'); + finalizeSpy.mockRejectedValue(expectedError); + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + indexer.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + indexer.end(); + }); + }); +}); diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts new file mode 100644 index 0000000000..b368dd8184 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts @@ -0,0 +1,125 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assertError } from '@backstage/errors'; +import { IndexableDocument } from '@backstage/search-common'; +import { Writable } from 'stream'; + +/** + * @beta + */ +export type BatchSearchEngineOptions = { + batchSize: number; +}; + +/** + * Base class encapsulating batch-based stream processing. Useful as a base + * class for search engine indexers. + * @beta + */ +export abstract class BatchSearchEngineIndexer extends Writable { + private batchSize: number; + private currentBatch: IndexableDocument[] = []; + private initialized: Promise; + + constructor(options: BatchSearchEngineOptions) { + super({ objectMode: true }); + this.batchSize = options.batchSize; + + // @todo Once node v15 is minimum, convert to _construct implementation. + this.initialized = new Promise(done => { + // Necessary to allow concrete implementation classes to construct + // themselves before calling their initialize() methods. + setImmediate(async () => { + try { + await this.initialize(); + done(undefined); + } catch (e) { + assertError(e); + done(e); + } + }); + }); + } + + /** + * Receives an array of indexable documents (of size this.batchSize) which + * should be written to the search engine. This method won't be called again + * at least until it resolves. + */ + public abstract index(documents: IndexableDocument[]): Promise; + + /** + * Any asynchronous setup tasks can be performed here. + */ + public abstract initialize(): Promise; + + /** + * Any asynchronous teardown tasks can be performed here. + */ + public abstract finalize(): Promise; + + /** + * Encapsulates batch stream write logic. + * @internal + */ + async _write( + doc: IndexableDocument, + _e: any, + done: (error?: Error | null) => void, + ) { + // Wait for init before proceeding. Throw error if initialization failed. + const maybeError = await this.initialized; + if (maybeError) { + done(maybeError); + return; + } + + this.currentBatch.push(doc); + if (this.currentBatch.length < this.batchSize) { + done(); + return; + } + + try { + await this.index(this.currentBatch); + this.currentBatch = []; + done(); + } catch (e) { + assertError(e); + done(e); + } + } + + /** + * Encapsulates finalization and final error handling logic. + * @internal + */ + async _final(done: (error?: Error | null) => void) { + try { + // Index any remaining documents. + if (this.currentBatch.length) { + await this.index(this.currentBatch); + this.currentBatch = []; + } + await this.finalize(); + done(); + } catch (e) { + assertError(e); + done(e); + } + } +} diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts new file mode 100644 index 0000000000..3b045dcddf --- /dev/null +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts @@ -0,0 +1,157 @@ +/* + * 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 { IndexableDocument } from '@backstage/search-common'; +import { DecoratorBase } from './DecoratorBase'; +import { TestPipeline } from '../test-utils'; + +const decorateSpy = jest.fn().mockResolvedValue(undefined); +const initializeSpy = jest.fn().mockResolvedValue(undefined); +const finalizeSpy = jest.fn().mockResolvedValue(undefined); + +class ConcreteDecorator extends DecoratorBase { + public initialize(): Promise { + return initializeSpy(); + } + public decorate( + document: IndexableDocument, + ): Promise { + return decorateSpy(document); + } + public finalize(): Promise { + return finalizeSpy(); + } +} + +describe('DecoratorBase', () => { + const document = { + title: 'Some Document', + text: 'Some document text.', + location: '/some/location', + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should work end-to-end', async () => { + decorateSpy.mockImplementation(doc => ({ + ...doc, + transformed: true, + })); + + const decorator = new ConcreteDecorator(); + const { documents } = await TestPipeline.withSubject(decorator) + .withDocuments([document, document, document]) + .execute(); + + expect(documents.length).toBe(3); + expect((documents[0] as unknown as any).transformed).toBe(true); + expect((documents[1] as unknown as any).transformed).toBe(true); + expect((documents[2] as unknown as any).transformed).toBe(true); + }); + + it('should allow filtering', async () => { + decorateSpy.mockResolvedValue(undefined); + + const decorator = new ConcreteDecorator(); + const { documents } = await TestPipeline.withSubject(decorator) + .withDocuments([document, document, document]) + .execute(); + + expect(decorateSpy).toHaveBeenCalledTimes(3); + expect(documents.length).toBe(0); + }); + + it('should allow fanning', async () => { + decorateSpy.mockImplementation(doc => { + return [doc, doc]; + }); + + const decorator = new ConcreteDecorator(); + const { documents } = await TestPipeline.withSubject(decorator) + .withDocuments([document, document, document]) + .execute(); + + expect(decorateSpy).toHaveBeenCalledTimes(3); + expect(documents.length).toBe(6); + }); + + it('should call initialize at construction', () => { + // @ts-expect-error + const _indexer = new ConcreteDecorator(); + + return new Promise(done => { + // Allow initialization to complete. + setImmediate(() => { + expect(initializeSpy).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('should emit error if initialization throws', () => { + // Cause the initializer to throw. + const expectedError = new Error('some error'); + initializeSpy.mockRejectedValue(expectedError); + const decorator = new ConcreteDecorator(); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + decorator.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + // Write a document to force the error state to become known. + decorator.write(document); + }); + }); + + it('should emit error if index throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('decorate error'); + decorateSpy.mockRejectedValue(expectedError); + const decorator = new ConcreteDecorator(); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + decorator.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + decorator.write(document); + }); + }); + + it('should emit error if finalize throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('finalize error'); + finalizeSpy.mockRejectedValue(expectedError); + const decorator = new ConcreteDecorator(); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + decorator.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + decorator.end(); + }); + }); +}); diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.ts new file mode 100644 index 0000000000..382ce443e3 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.ts @@ -0,0 +1,127 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assertError } from '@backstage/errors'; +import { IndexableDocument } from '@backstage/search-common'; +import { Transform } from 'stream'; + +/** + * Base class encapsulating simple async transformations. Useful as a base + * class for Backstage search decorators. + * @beta + */ +export abstract class DecoratorBase extends Transform { + private initialized: Promise; + + constructor() { + super({ objectMode: true }); + + // @todo Once node v15 is minimum, convert to _construct implementation. + this.initialized = new Promise(done => { + // Necessary to allow concrete implementation classes to construct + // themselves before calling their initialize() methods. + setImmediate(async () => { + try { + await this.initialize(); + done(undefined); + } catch (e) { + assertError(e); + done(e); + } + }); + }); + } + + /** + * Any asynchronous setup tasks can be performed here. + */ + public abstract initialize(): Promise; + + /** + * Receives a single indexable document. In your decorate method, you can: + * + * - Resolve `undefined` to indicate the record should be omitted. + * - Resolve a single modified document, which could contain new fields, + * edited fields, or removed fields. + * - Resolve an array of indexable documents, if the purpose if the decorator + * is to convert one document into multiple derivative documents. + */ + public abstract decorate( + document: IndexableDocument, + ): Promise; + + /** + * Any asynchronous teardown tasks can be performed here. + */ + public abstract finalize(): Promise; + + /** + * Encapsulates simple transform stream logic. + * @internal + */ + async _transform( + document: IndexableDocument, + _: any, + done: (error?: Error | null) => void, + ) { + // Wait for init before proceeding. Throw error if initialization failed. + const maybeError = await this.initialized; + if (maybeError) { + done(maybeError); + return; + } + + try { + const decorated = await this.decorate(document); + + // If undefined was returned, omit the record and move on. + if (decorated === undefined) { + done(); + return; + } + + // If an array of documents was given, push them all. + if (Array.isArray(decorated)) { + decorated.forEach(doc => { + this.push(doc); + }); + done(); + return; + } + + // Otherwise, just push the decorated document. + this.push(decorated); + done(); + } catch (e) { + assertError(e); + done(e); + } + } + + /** + * Encapsulates finalization and final error handling logic. + * @internal + */ + async _final(done: (error?: Error | null) => void) { + try { + await this.finalize(); + done(); + } catch (e) { + assertError(e); + done(e); + } + } +} diff --git a/plugins/search-backend-node/src/indexing/index.ts b/plugins/search-backend-node/src/indexing/index.ts new file mode 100644 index 0000000000..e86235df16 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { BatchSearchEngineIndexer } from './BatchSearchEngineIndexer'; +export { DecoratorBase } from './DecoratorBase'; +export type { BatchSearchEngineOptions } from './BatchSearchEngineIndexer'; diff --git a/plugins/search-backend-node/src/test-utils/TestPipeline.ts b/plugins/search-backend-node/src/test-utils/TestPipeline.ts new file mode 100644 index 0000000000..c0c2bf5f89 --- /dev/null +++ b/plugins/search-backend-node/src/test-utils/TestPipeline.ts @@ -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 { IndexableDocument } from '@backstage/search-common'; +import { pipeline, Readable, Transform, Writable } from 'stream'; + +/** + * Object resolved after a test pipeline is executed. + * @beta + */ +export type TestPipelineResult = { + /** + * If an error was emitted by the pipeline, it will be set here. + */ + error: unknown; + + /** + * A list of documents collected at the end of the pipeline. If the subject + * under test is an indexer, this will be an empty array (because your + * indexer should have received the documents instead). + */ + documents: IndexableDocument[]; +}; + +/** + * Test utility for Backstage Search collators, decorators, and indexers. + * @beta + */ +export class TestPipeline { + private collator?: Readable; + private decorator?: Transform; + private indexer?: Writable; + + private constructor({ + collator, + decorator, + indexer, + }: { + collator?: Readable; + decorator?: Transform; + indexer?: Writable; + }) { + this.collator = collator; + this.decorator = decorator; + this.indexer = indexer; + } + + /** + * Provide the collator, decorator, or indexer to be tested. + */ + static withSubject(subject: Readable | Transform | Writable) { + if (subject instanceof Transform) { + return new TestPipeline({ decorator: subject }); + } + + if (subject instanceof Readable) { + return new TestPipeline({ collator: subject }); + } + + if (subject instanceof Writable) { + return new TestPipeline({ indexer: subject }); + } + + throw new Error( + 'Unknown test subject: are you passing a readable, writable, or transform stream?', + ); + } + + /** + * Provide documents for testing decorators and indexers. + */ + withDocuments(documents: IndexableDocument[]): TestPipeline { + if (this.collator) { + throw new Error('Cannot provide documents when testing a collator.'); + } + + // Set a naive readable stream that just pushes all given documents. + this.collator = new Readable({ objectMode: true }); + this.collator._read = () => {}; + process.nextTick(() => { + documents.forEach(document => { + this.collator!.push(document); + }); + this.collator!.push(null); + }); + + return this; + } + + /** + * Execute the test pipeline so that you can make assertions about the result + * or behavior of the given test subject. + */ + async execute(): Promise { + const documents: IndexableDocument[] = []; + if (!this.collator) { + throw new Error( + 'Cannot execute pipeline without a collator or documents', + ); + } + + // If we are here and there is no indexer, we are testing a collator or a + // decorator. Set up a naive writable that captures documents in memory. + if (!this.indexer) { + this.indexer = new Writable({ objectMode: true }); + this.indexer._write = (document: IndexableDocument, _, done) => { + documents.push(document); + done(); + }; + } + + return new Promise(done => { + const pipes: (Readable | Transform | Writable)[] = [this.collator!]; + if (this.decorator) { + pipes.push(this.decorator); + } + pipes.push(this.indexer!); + + pipeline(pipes, error => { + done({ + error, + documents, + }); + }); + }); + } +} diff --git a/plugins/catalog-backend/src/providers/index.ts b/plugins/search-backend-node/src/test-utils/index.ts similarity index 79% rename from plugins/catalog-backend/src/providers/index.ts rename to plugins/search-backend-node/src/test-utils/index.ts index 3b4a1c4b7e..185d25433b 100644 --- a/plugins/catalog-backend/src/providers/index.ts +++ b/plugins/search-backend-node/src/test-utils/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 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,8 +14,5 @@ * limitations under the License. */ -export type { - EntityProvider, - EntityProviderConnection, - EntityProviderMutation, -} from './types'; +export { TestPipeline } from './TestPipeline'; +export type { TestPipelineResult } from './TestPipeline'; diff --git a/plugins/search-backend-node/src/types.ts b/plugins/search-backend-node/src/types.ts index df83357f6c..68ccfe4c7e 100644 --- a/plugins/search-backend-node/src/types.ts +++ b/plugins/search-backend-node/src/types.ts @@ -14,10 +14,24 @@ * limitations under the License. */ -import { DocumentCollator, DocumentDecorator } from '@backstage/search-common'; +import { + DocumentCollatorFactory, + DocumentDecoratorFactory, + SearchEngine, +} from '@backstage/search-common'; +import { Logger } from 'winston'; + +/** + * @beta + */ +export type IndexBuilderOptions = { + searchEngine: SearchEngine; + logger: Logger; +}; /** * Parameters required to register a collator. + * @beta */ export interface RegisterCollatorParameters { /** @@ -26,17 +40,18 @@ export interface RegisterCollatorParameters { defaultRefreshIntervalSeconds: number; /** - * The collator class responsible for returning all documents of the given type. + * The class responsible for returning the document collator of the given type. */ - collator: DocumentCollator; + factory: DocumentCollatorFactory; } /** * Parameters required to register a decorator + * @beta */ export interface RegisterDecoratorParameters { /** - * The decorator class responsible for appending or modifying documents of the given type(s). + * The class responsible for returning the decorator which appends, modifies, or filters documents. */ - decorator: DocumentDecorator; + factory: DocumentDecoratorFactory; } diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 218615c66b..a802848287 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-search-backend +## 0.4.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-permission-node@0.5.2 + - @backstage/plugin-auth-node@0.1.3 + +## 0.4.4 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/search-common@0.2.4 + - @backstage/types@0.1.3 + - @backstage/plugin-auth-node@0.1.2 + - @backstage/plugin-permission-common@0.5.1 + - @backstage/plugin-permission-node@0.5.1 + - @backstage/plugin-search-backend-node@0.4.7 + +## 0.4.3 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/plugin-auth-node@0.1.1 + - @backstage/plugin-permission-common@0.5.0 + - @backstage/config@0.1.14 + - @backstage/search-common@0.2.3 + - @backstage/types@0.1.2 + - @backstage/plugin-permission-node@0.5.0 + - @backstage/plugin-search-backend-node@0.4.6 + ## 0.4.2 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 117e484c62..cdfb1afead 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.2", + "version": "0.4.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,25 +10,28 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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-node": "^0.1.0", - "@backstage/plugin-permission-common": "^0.4.0-next.0", - "@backstage/plugin-permission-node": "^0.4.3", - "@backstage/plugin-search-backend-node": "^0.4.5", - "@backstage/types": "^0.1.1", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/search-common": "^0.2.4", + "@backstage/plugin-auth-node": "^0.1.3", + "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-permission-node": "^0.5.2", + "@backstage/plugin-search-backend-node": "^0.4.7", + "@backstage/types": "^0.1.3", "@types/express": "^4.17.6", "dataloader": "^2.0.0", "express": "^4.17.1", @@ -40,7 +43,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts index 0a7b45e164..137ffc24f1 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts @@ -62,7 +62,7 @@ describe('AuthorizedSearchEngine', () => { setTranslator: () => { throw new Error('Function not implemented. 1'); }, - index: () => { + getIndexer: () => { throw new Error('Function not implemented.2'); }, query: mockedQuery, diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts index d0488af00d..82ed061196 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts @@ -25,7 +25,6 @@ import { } from '@backstage/plugin-permission-common'; import { DocumentTypeInfo, - IndexableDocument, QueryRequestOptions, QueryTranslator, SearchEngine, @@ -35,6 +34,7 @@ import { } from '@backstage/search-common'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; +import { Writable } from 'stream'; export function decodePageCursor(pageCursor?: string): { page: number } { if (!pageCursor) { @@ -78,8 +78,8 @@ export class AuthorizedSearchEngine implements SearchEngine { this.searchEngine.setTranslator(translator); } - async index(type: string, documents: IndexableDocument[]): Promise { - this.searchEngine.index(type, documents); + async getIndexer(type: string): Promise { + return this.searchEngine.getIndexer(type); } async query( diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 0a94250aeb..bdf46a240b 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -105,7 +105,7 @@ describe('createRouter', () => { beforeAll(async () => { const logger = getVoidLogger(); mockSearchEngine = { - index: jest.fn(), + getIndexer: jest.fn(), setTranslator: jest.fn(), query: jest.fn(), }; diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index bb4877b6da..a0e80eaeee 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-search +## 0.7.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.7.0 + +### Minor Changes + +- f986369b2a: **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 ( + + ); + }; + ``` + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/search-common@0.2.3 + - @backstage/theme@0.2.15 + - @backstage/types@0.1.2 + ## 0.6.2 ### Patch Changes diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index a45f02564f..10ce787a6c 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -145,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) @@ -201,6 +209,7 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { // @public (undocumented) export const SearchModal: ({ open, + hidden, toggleModal, }: SearchModalProps) => JSX.Element; @@ -208,9 +217,8 @@ export const SearchModal: ({ // // @public (undocumented) export interface SearchModalProps { - // (undocumented) + hidden?: boolean; open?: boolean; - // (undocumented) toggleModal: () => void; } @@ -320,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 34baf41306..4543aa8dcd 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.2", + "version": "0.7.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -20,32 +23,32 @@ "backstage" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.14", - "@backstage/search-common": "^0.2.2", - "@backstage/theme": "^0.2.14", - "@backstage/types": "^0.1.1", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/search-common": "^0.2.4", + "@backstage/theme": "^0.2.15", + "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "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-text-truncate": "^0.18.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -53,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/search/src/components/LegacySearchPage/LegacySearchResult.tsx b/plugins/search/src/components/LegacySearchPage/LegacySearchResult.tsx index f48ffb61d7..ba92dba710 100644 --- a/plugins/search/src/components/LegacySearchPage/LegacySearchResult.tsx +++ b/plugins/search/src/components/LegacySearchPage/LegacySearchResult.tsx @@ -20,7 +20,7 @@ import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Filters, FiltersButton, FiltersState } from './Filters'; -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { EmptyState, @@ -156,7 +156,7 @@ export const SearchResult = ({ searchQuery }: SearchResultProps) => { : undefined, url: `/catalog/${ entity.metadata.namespace?.toLocaleLowerCase('en-US') || - ENTITY_DEFAULT_NAMESPACE + DEFAULT_NAMESPACE }/${entity.kind.toLocaleLowerCase('en-US')}/${entity.metadata.name}`, })); }, []); 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 5f87df9a3a..44f20e05d1 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-sentry +## 0.3.38 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.3.37 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.3.36 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 48137dc5e1..1d6db3a846 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.36", + "version": "0.3.38", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,21 +25,21 @@ "sentry" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index 11bc03104a..f60d458dd0 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-shortcuts +## 0.2.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.0 + +### Minor Changes + +- dc9fd0acf9: Use Avatar instead of custom icon for ShortcutIcon + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/theme@0.2.15 + - @backstage/types@0.1.2 + ## 0.1.22 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index d3bcb14f15..2407f4b84b 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.22", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,21 +10,24 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", - "@backstage/types": "^0.1.1", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/theme": "^0.2.15", + "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/src/ShortcutIcon.tsx b/plugins/shortcuts/src/ShortcutIcon.tsx index 94a894fcb0..be78928452 100644 --- a/plugins/shortcuts/src/ShortcutIcon.tsx +++ b/plugins/shortcuts/src/ShortcutIcon.tsx @@ -15,33 +15,28 @@ */ import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Avatar from '@material-ui/core/Avatar'; type Props = { text: string; color: string; }; -export const ShortcutIcon = ({ text, color }: Props) => { - const size = 28; - return ( - - - - {text} - - - ); +const useStyles = makeStyles(theme => ({ + avatar: (props: Props) => ({ + color: theme.palette.getContrastText(props.color), + backgroundColor: props.color, + width: 28, + height: 28, + fontWeight: 'bold', + fontSize: 13, + filter: 'contrast(150%) brightness(1.4)', + }), +})); + +export const ShortcutIcon = (props: Props) => { + const classes = useStyles(props); + + return {props.text}; }; diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx index e080026f00..528eb13efa 100644 --- a/plugins/shortcuts/src/ShortcutItem.test.tsx +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -24,7 +24,6 @@ import { renderInTestApp, wrapInTestApp, } from '@backstage/test-utils'; -import { pageTheme } from '@backstage/theme'; import { SidebarContext } from '@backstage/core-components'; describe('ShortcutItem', () => { @@ -77,27 +76,4 @@ describe('ShortcutItem', () => { expect(screen.getByText('MT')).toBeInTheDocument(); }); }); - - it('gets the color based on the theme', async () => { - const { rerender } = await renderInTestApp( - , - ); - - expect(document.querySelector('circle')?.getAttribute('fill')).toEqual( - pageTheme.tool.colors[0], - ); - - const newShortcut: Shortcut = { - id: 'id', - url: '/catalog', - title: 'some title', - }; - rerender(wrapInTestApp()); - - await waitFor(() => { - expect(document.querySelector('circle')?.getAttribute('fill')).toEqual( - pageTheme.home.colors[0], - ); - }); - }); }); diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index c0de393aab..53209faa93 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-sonarqube +## 0.3.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.2.17 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.2.16 ### Patch Changes diff --git a/plugins/sonarqube/api-report.md b/plugins/sonarqube/api-report.md index b23ed65ddd..a7549a689c 100644 --- a/plugins/sonarqube/api-report.md +++ b/plugins/sonarqube/api-report.md @@ -16,7 +16,6 @@ export const EntitySonarQubeCard: ({ variant, duplicationRatings, }: { - entity?: Entity | undefined; variant?: InfoCardVariants | undefined; duplicationRatings?: | { @@ -38,7 +37,6 @@ export const SonarQubeCard: ({ variant, duplicationRatings, }: { - entity?: Entity | undefined; variant?: InfoCardVariants | undefined; duplicationRatings?: DuplicationRating[] | undefined; }) => JSX.Element; @@ -52,5 +50,5 @@ export { sonarQubePlugin }; // Warnings were encountered during analysis: // -// src/components/SonarQubeCard/SonarQubeCard.d.ts:11:5 - (ae-forgotten-export) The symbol "DuplicationRating" needs to be exported by the entry point index.d.ts +// src/components/SonarQubeCard/SonarQubeCard.d.ts:9:5 - (ae-forgotten-export) The symbol "DuplicationRating" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index fb2de64ffe..38aada552f 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.2.16", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -23,21 +26,21 @@ "sonarcloud" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -50,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx index ca3ddbda76..4527c2a3e3 100644 --- a/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx +++ b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; import { Chip, Grid } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; @@ -90,7 +89,6 @@ export const SonarQubeCard = ({ variant = 'gridItem', duplicationRatings = defaultDuplicationRatings, }: { - entity?: Entity; variant?: InfoCardVariants; duplicationRatings?: DuplicationRating[]; }) => { diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index badc0f06fc..8667f46749 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-splunk-on-call +## 0.3.24 + +### Patch Changes + +- 02faa6781b: The Splunk On-Call plugin now supports an optional `readOnly` property (``) for suppressing the rendering of incident trigger-acknowledge-resolve controls from the Backstage UI. +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.3.23 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + ## 0.3.22 ### Patch Changes diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 0200cacc50..5078dfa38b 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -45,6 +45,14 @@ const overviewContent = ( ``` +### `readOnly` mode + +To suppress the rendering of the actionable create-acknowledge-resolve incident buttons and UI controls, the `EntitySplunkOnCallCard` can also be instantiated in `readOnly` mode: + +```ts + +``` + ## Client configuration In order to be able to perform certain actions (create-acknowledge-resolve an action), you need to provide a REST Endpoint. @@ -74,6 +82,19 @@ proxy: In addition, to make certain API calls (trigger-resolve-acknowledge an incident) you need to add the `PATCH` method to the backend `cors` methods list: `[GET, POST, PUT, DELETE, PATCH]`. +**WARNING**: In current implementation, the Splunk OnCall plugin requires the `/splunk-on-call` proxy endpoint be exposed by the Backstage backend as an unprotected endpoint, in effect enabling Splunk OnCall API access using the configured `SPLUNK_ON_CALL_API_KEY` for any user or process with access to the `/splunk-on-call` Backstage backend endpoint. See below for further configuration options enabling protection of this endpoint. If you regard this as problematic, consider using the plugin in `readOnly` mode (``) using the following proxy configuration: + +```yaml +proxy: + '/splunk-on-call': + target: https://api.victorops.com/api-public + headers: + X-VO-Api-Id: ${SPLUNK_ON_CALL_API_ID} + X-VO-Api-Key: ${SPLUNK_ON_CALL_API_KEY} + # prohibit the `/splunk-on-call` proxy endpoint from servicing non-GET requests + allowedMethods: ['GET'] +``` + ### Adding your team name to the entity annotation The information displayed for each entity is based on either an associated team name or an associated routing key. diff --git a/plugins/splunk-on-call/api-report.md b/plugins/splunk-on-call/api-report.md index 8e8076d9e8..60ec38ca6d 100644 --- a/plugins/splunk-on-call/api-report.md +++ b/plugins/splunk-on-call/api-report.md @@ -12,10 +12,13 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { RouteRef } from '@backstage/core-plugin-api'; +// Warning: (ae-forgotten-export) The symbol "EntitySplunkOnCallCardProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EntitySplunkOnCallCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntitySplunkOnCallCard: () => JSX.Element; +export const EntitySplunkOnCallCard: ( + props: EntitySplunkOnCallCardProps, +) => JSX.Element; // Warning: (ae-missing-release-tag) "isSplunkOnCallAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 1bc8948d95..7d8f6b5bdf 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.22", + "version": "0.3.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,21 +24,21 @@ "splunk-on-call" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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/theme": "^0.2.14", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +51,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@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/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index a83db136e4..0688fb8237 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -131,6 +131,32 @@ describe('SplunkOnCallCard', () => { expect(getByText('Empty escalation policy')).toBeInTheDocument(); }); + it('does not render a "Create incident" link in read only mode', async () => { + mockSplunkOnCallApi.getUsers = jest + .fn() + .mockImplementationOnce(async () => [MOCKED_USER]); + mockSplunkOnCallApi.getTeams = jest + .fn() + .mockImplementation(async () => [MOCK_TEAM_NO_INCIDENTS]); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(() => getByText('Create Incident')).toThrow(); + await waitFor( + () => expect(getByText('Nice! No incidents found!')).toBeInTheDocument(), + { timeout: 2000 }, + ); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + }); + it('handles a "splunk.com/on-call-routing-key" annotation', async () => { mockSplunkOnCallApi.getUsers = jest .fn() diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 4a6f983636..4f6bcd9a0e 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -109,7 +109,14 @@ const useStyles = makeStyles({ }, }); -export const EntitySplunkOnCallCard = () => { +/** @public */ +export type EntitySplunkOnCallCardProps = { + readOnly?: boolean; +}; + +/** @public */ +export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { + const { readOnly } = props; const classes = useStyles(); const config = useApi(configApiRef); const api = useApi(splunkOnCallApiRef); @@ -218,7 +225,11 @@ export const EntitySplunkOnCallCard = () => { return ( <> - + {usersHashMap && team && ( )} @@ -259,7 +270,7 @@ export const EntitySplunkOnCallCard = () => { , , ]} /> diff --git a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx index 4c9975bd06..d8b6a56efc 100644 --- a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx +++ b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx @@ -64,6 +64,7 @@ type Props = { team: string; incident: Incident; onIncidentAction: () => void; + readOnly: boolean; }; const IncidentPhaseStatus = ({ @@ -135,6 +136,7 @@ const IncidentAction = ({ export const IncidentListItem = ({ incident, + readOnly, onIncidentAction, team, }: Props) => { @@ -241,12 +243,14 @@ export const IncidentListItem = ({ {incident.incidentLink && incident.incidentNumber && ( - + {!readOnly && ( + + )} { const { getByText, queryByTestId } = render( wrapInTestApp( - + , ), ); @@ -68,7 +68,7 @@ describe('Incidents', () => { } = render( wrapInTestApp( - + , ), ); @@ -90,6 +90,40 @@ describe('Incidents', () => { expect(getAllByTitle('View in Splunk On-Call').length).toEqual(1); }); + it('does not render incident action buttons in read only mode', async () => { + mockSplunkOnCallApi.getIncidents.mockResolvedValue([MOCK_INCIDENT]); + mockSplunkOnCallApi.getTeams.mockResolvedValue([MOCK_TEAM]); + + const { getByText, getAllByTitle, getByLabelText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + await waitFor( + () => + expect( + getByText('user', { + exact: false, + }), + ).toBeInTheDocument(), + { timeout: 2000 }, + ); + expect(getByText('test-incident')).toBeInTheDocument(); + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(() => getAllByTitle('Acknowledge')).toThrow( + 'Unable to find an element with the title: Acknowledge.', + ); + expect(() => getAllByTitle('Resolve')).toThrow( + 'Unable to find an element with the title: Resolve.', + ); + + // assert links, mailto and hrefs, date calculation + expect(getAllByTitle('View in Splunk On-Call').length).toEqual(1); + }); + it('Handle errors', async () => { mockSplunkOnCallApi.getIncidents.mockRejectedValueOnce( new Error('Error occurred'), @@ -99,7 +133,7 @@ describe('Incidents', () => { const { getByText, queryByTestId } = render( wrapInTestApp( - + , ), ); diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx index 3c557773fd..e2ec9609bd 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx @@ -49,9 +49,10 @@ const useStyles = makeStyles((theme: Theme) => type Props = { refreshIncidents: boolean; team: string; + readOnly: boolean; }; -export const Incidents = ({ refreshIncidents, team }: Props) => { +export const Incidents = ({ readOnly, refreshIncidents, team }: Props) => { const classes = useStyles(); const api = useApi(splunkOnCallApiRef); @@ -108,6 +109,7 @@ export const Incidents = ({ refreshIncidents, team }: Props) => { key={index} team={team} incident={incident} + readOnly={readOnly} /> )) )} diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index 3128854594..b34cb336cd 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.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/plugin-tech-insights-node@0.2.5 + +## 0.1.10 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/plugin-tech-insights-common@0.2.3 + - @backstage/plugin-tech-insights-node@0.2.4 + +## 0.1.9 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/config@0.1.14 + - @backstage/plugin-tech-insights-common@0.2.2 + - @backstage/plugin-tech-insights-node@0.2.3 + ## 0.1.8 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index fc2c6b7add..5c618d4172 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.8", + "version": "0.1.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin-module" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,20 +25,20 @@ "scorecard" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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.2", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-tech-insights-common": "^0.2.3", + "@backstage/plugin-tech-insights-node": "^0.2.5", "ajv": "^7.0.3", "json-rules-engine": "^6.1.2", "lodash": "^4.17.21", @@ -43,7 +46,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2", + "@backstage/cli": "^0.14.1", "@types/node-cron": "^3.0.1" }, "files": [ diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index 36e7aa97a1..34dfee6ccb 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.7 + +### Patch Changes + +- 67a7c02d26: Remove usages of `EntityRef` and `parseEntityName` from `@backstage/catalog-model` +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/plugin-tech-insights-node@0.2.5 + +## 0.2.6 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-client@0.7.1 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/plugin-tech-insights-common@0.2.3 + - @backstage/plugin-tech-insights-node@0.2.4 + +## 0.2.5 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 538ca90790: Use updated type names from `@backstage/catalog-client` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/catalog-client@0.7.0 + - @backstage/errors@0.2.1 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/plugin-tech-insights-common@0.2.2 + - @backstage/plugin-tech-insights-node@0.2.3 + ## 0.2.4 ### Patch Changes diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index 5a60e478d8..c44aaa138c 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.4", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,22 +25,22 @@ "reporting" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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.2", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-tech-insights-common": "^0.2.3", + "@backstage/plugin-tech-insights-node": "^0.2.5", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -51,8 +54,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.17", - "@backstage/cli": "^0.13.2", + "@backstage/backend-test-utils": "^0.1.19", + "@backstage/cli": "^0.14.1", "@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..14d0728cf6 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', @@ -57,6 +57,7 @@ const defaultEntityListResponse: CatalogListResponse = { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'group:default/my-team', target: { name: 'team-a', kind: 'group', namespace: 'default' }, }, ], 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..374bda2569 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', @@ -57,6 +57,7 @@ const defaultEntityListResponse: CatalogListResponse = { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'group:default/team-a', target: { name: 'team-a', kind: 'group', namespace: 'default' }, }, ], 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..b4fa8ba67c 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', @@ -57,6 +57,7 @@ const defaultEntityListResponse: CatalogListResponse = { relations: [ { type: RELATION_OWNED_BY, + targetRef: 'group:default/team-a', target: { name: 'team-a', kind: 'group', namespace: 'default' }, }, ], diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts index 0b7013295f..ddb90b75cd 100644 --- a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts @@ -26,7 +26,7 @@ import { rsort } from 'semver'; import { groupBy, omit } from 'lodash'; import { DateTime } from 'luxon'; import { Logger } from 'winston'; -import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model'; +import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; import { isMaxItems, isTtl } from '../fact/factRetrievers/utils'; type Transaction = Knex.Transaction; @@ -161,7 +161,7 @@ export class TechInsightsDatabase implements TechInsightsStore { return groupBy( results.map(it => { - const { namespace, kind, name } = parseEntityName(it.entity); + const { namespace, kind, name } = parseEntityRef(it.entity); const timestamp = typeof it.timestamp === 'string' ? DateTime.fromISO(it.timestamp) @@ -253,7 +253,7 @@ export class TechInsightsDatabase implements TechInsightsStore { private dbFactRowsToTechInsightFacts(rows: RawDbFactRow[]) { return rows.reduce((acc, it) => { - const { namespace, kind, name } = parseEntityName(it.entity); + const { namespace, kind, name } = parseEntityRef(it.entity); const timestamp = typeof it.timestamp === 'string' ? DateTime.fromISO(it.timestamp) diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index 22a80c2d79..b13802358f 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -28,8 +28,7 @@ import { DateTime } from 'luxon'; import { PersistenceContext } from './persistence/persistenceContext'; import { EntityName, - EntityRef, - parseEntityName, + parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { errorHandler } from '@backstage/backend-common'; @@ -130,7 +129,7 @@ export async function createRouter< */ router.get('/facts/latest', async (req, res) => { const { entity } = req.query; - const { namespace, kind, name } = parseEntityName(entity as EntityRef); + const { namespace, kind, name } = parseEntityRef(entity as string); const ids = req.query.ids as string[]; return res.send( await techInsightsStore.getLatestFactsByIds( @@ -145,7 +144,7 @@ export async function createRouter< */ router.get('/facts/range', async (req, res) => { const { entity } = req.query; - const { namespace, kind, name } = parseEntityName(entity as EntityRef); + const { namespace, kind, name } = parseEntityRef(entity as string); const ids = req.query.ids as string[]; const startDatetime = DateTime.fromISO(req.query.startDatetime as string); diff --git a/plugins/tech-insights-common/CHANGELOG.md b/plugins/tech-insights-common/CHANGELOG.md index 947c22756f..e895a82e6c 100644 --- a/plugins/tech-insights-common/CHANGELOG.md +++ b/plugins/tech-insights-common/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-tech-insights-common +## 0.2.3 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/types@0.1.3 + +## 0.2.2 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/types@0.1.2 + ## 0.2.1 ### Patch Changes diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index a0b840da74..f20310cc0c 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-common", - "version": "0.2.1", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,20 +25,20 @@ ], "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" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { "@types/luxon": "^2.0.5", "luxon": "^2.0.2", - "@backstage/types": "^0.1.1" + "@backstage/types": "^0.1.3" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0" + "@backstage/cli": "^0.14.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md index c38c36ecd9..cb019ccb99 100644 --- a/plugins/tech-insights-node/CHANGELOG.md +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-tech-insights-node +## 0.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.11.0 + +## 0.2.4 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/config@0.1.15 + - @backstage/plugin-tech-insights-common@0.2.3 + +## 0.2.3 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/config@0.1.14 + - @backstage/plugin-tech-insights-common@0.2.2 + ## 0.2.2 ### Patch Changes diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index 05c6f10071..b3b0c7d6b8 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.2", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,24 +24,24 @@ "tech-insights" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.7", - "@backstage/config": "^0.1.13", - "@backstage/plugin-tech-insights-common": "^0.2.1", + "@backstage/backend-common": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/plugin-tech-insights-common": "^0.2.3", "@types/luxon": "^2.0.5", "luxon": "^2.0.2", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2" + "@backstage/cli": "^0.14.1" }, "files": [ "dist" diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index af16d5f347..30f21e45bd 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-tech-insights +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + +## 0.1.9 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/catalog-model@0.10.0 + - @backstage/theme@0.2.15 + - @backstage/types@0.1.2 + - @backstage/plugin-tech-insights-common@0.2.2 + ## 0.1.8 ### Patch Changes diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index b7b1b6877c..99d4eeefad 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.8", + "version": "0.1.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,25 +9,28 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/errors": "^0.2.0", - "@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", + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-tech-insights-common": "^0.2.3", + "@backstage/theme": "^0.2.15", + "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index bafe2212cf..88913e12e4 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-tech-radar +## 0.5.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.10 + - @backstage/core-plugin-api@0.7.0 + +## 0.5.6 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/theme@0.2.15 + ## 0.5.5 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 29601d1755..0ea04212a5 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.5", + "version": "0.5.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -21,19 +24,19 @@ "backstage" ], "scripts": { - "build": "backstage-cli plugin:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean", - "start": "backstage-cli plugin:serve" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-components": "^0.8.8", - "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -46,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index b05e8a6dad..2e253f5d0c 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-techdocs-backend +## 0.14.0 + +### Minor Changes + +- a925ba8385: BREAKING: constructor based initialization of DefaultTechDocsCollator now deprecated. Use static fromConfig method instead. + + ```diff + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: new DefaultTechDocsCollator({ + + collator: DefaultTechDocsCollator.fromConfig(config, { + discovery, + logger, + tokenManager, + }), + }); + ``` + + Note: in an upcoming release, TechDocs backend's /sync/:namespace/:kind/:name endpoint will only respond to text/event-stream-based requests. Update any custom code at your organization accordingly. + +### Patch Changes + +- 91eb01b5cf: Optimize DefaultTechDocsCollator get entities. +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/catalog-client@0.7.2 + - @backstage/techdocs-common@0.11.10 + - @backstage/integration@0.7.5 + +## 0.13.5 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-client@0.7.1 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + - @backstage/search-common@0.2.4 + - @backstage/techdocs-common@0.11.9 + - @backstage/plugin-catalog-common@0.1.4 + +## 0.13.4 + +### Patch Changes + +- 453145abba: Do not use cross-fetch in the backend +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 811c710a21: Fix bug where tech docs collator stores search indices with wrong entity ref casing. Make the collator to conform legacyPathCasing configuration option. +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/catalog-client@0.7.0 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/search-common@0.2.3 + - @backstage/techdocs-common@0.11.8 + - @backstage/plugin-catalog-common@0.1.3 + ## 0.13.3 ### Patch Changes diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index cefb7d937b..155f096614 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -3,9 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { DocumentCollator } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/search-common'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { GeneratorBuilder } from '@backstage/techdocs-common'; import { Knex } from 'knex'; @@ -15,28 +18,21 @@ import { PluginCacheManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PreparerBuilder } from '@backstage/techdocs-common'; import { PublisherBase } from '@backstage/techdocs-common'; +import { Readable } from 'stream'; import { TechDocsDocument } from '@backstage/techdocs-common'; import { TokenManager } from '@backstage/backend-common'; -// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createRouter(options: RouterOptions): Promise; -// Warning: (ae-missing-release-tag) "DefaultTechDocsCollator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class DefaultTechDocsCollator implements DocumentCollator { - // @deprecated - constructor(options: TechDocsCollatorOptions); +// @public @deprecated +export class DefaultTechDocsCollator { // (undocumented) protected applyArgsToFormat( format: string, args: Record, ): string; // (undocumented) - protected discovery: PluginEndpointDiscovery; - // (undocumented) execute(): Promise; // (undocumented) static fromConfig( @@ -44,15 +40,32 @@ export class DefaultTechDocsCollator implements DocumentCollator { options: TechDocsCollatorOptions, ): DefaultTechDocsCollator; // (undocumented) - protected locationTemplate: string; + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission; +} + +// @public +export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + static fromConfig( + config: Config, + options: TechDocsCollatorFactoryOptions, + ): DefaultTechDocsCollatorFactory; + // (undocumented) + getCollator(): Promise; // (undocumented) readonly type: string; // (undocumented) readonly visibilityPermission: Permission; } -// Warning: (ae-missing-release-tag) "OutOfTheBoxDeploymentOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export interface DocsBuildStrategy { + // (undocumented) + shouldBuild(params: ShouldBuildParameters): Promise; +} + // @public export type OutOfTheBoxDeploymentOptions = { preparers: PreparerBuilder; @@ -63,10 +76,9 @@ export type OutOfTheBoxDeploymentOptions = { database?: Knex; config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; -// Warning: (ae-missing-release-tag) "RecommendedDeploymentOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type RecommendedDeploymentOptions = { publisher: PublisherBase; @@ -74,18 +86,31 @@ export type RecommendedDeploymentOptions = { discovery: PluginEndpointDiscovery; config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; -// Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type RouterOptions = | RecommendedDeploymentOptions | OutOfTheBoxDeploymentOptions; -// Warning: (ae-missing-release-tag) "TechDocsCollatorOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type ShouldBuildParameters = { + entity: Entity; +}; + +// @public +export type TechDocsCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger_2; + tokenManager: TokenManager; + locationTemplate?: string; + catalogClient?: CatalogApi; + parallelismLimit?: number; + legacyPathCasing?: boolean; +}; + +// @public export type TechDocsCollatorOptions = { discovery: PluginEndpointDiscovery; logger: Logger_2; diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 37b15ffbe0..20100f4848 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.3", + "version": "0.14.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,24 +25,24 @@ "techdocs" ], "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" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@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.2", - "@backstage/search-common": "^0.2.2", - "@backstage/techdocs-common": "^0.11.7", + "@backstage/backend-common": "^0.11.0", + "@backstage/catalog-client": "^0.7.2", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/plugin-catalog-common": "^0.1.4", + "@backstage/search-common": "^0.2.4", + "@backstage/techdocs-common": "^0.11.10", "@types/express": "^4.17.6", "dockerode": "^3.3.1", "express": "^4.17.1", @@ -52,8 +55,9 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/plugin-search-backend-node": "0.4.7", + "@backstage/test-utils": "^0.2.6", "@types/dockerode": "^3.3.0", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 66eb52c1aa..05e9c0ce52 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -15,7 +15,7 @@ */ import { Entity, - ENTITY_DEFAULT_NAMESPACE, + DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; @@ -111,8 +111,7 @@ export class DocsBuilder { try { storedEtag = ( await this.publisher.fetchTechDocsMetadata({ - namespace: - this.entity.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE, + namespace: this.entity.metadata.namespace ?? DEFAULT_NAMESPACE, kind: this.entity.kind, name: this.entity.metadata.name, }) diff --git a/plugins/techdocs-backend/src/index.ts b/plugins/techdocs-backend/src/index.ts index 12e7df1e9e..570da95092 100644 --- a/plugins/techdocs-backend/src/index.ts +++ b/plugins/techdocs-backend/src/index.ts @@ -25,10 +25,18 @@ export type { RouterOptions, RecommendedDeploymentOptions, OutOfTheBoxDeploymentOptions, + DocsBuildStrategy, + ShouldBuildParameters, } from './service'; -export { DefaultTechDocsCollator } from './search'; -export type { TechDocsCollatorOptions } from './search'; +export { + DefaultTechDocsCollator, + DefaultTechDocsCollatorFactory, +} from './search'; +export type { + TechDocsCollatorFactoryOptions, + TechDocsCollatorOptions, +} from './search'; /** * @deprecated Use directly from @backstage/techdocs-common diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index 7e7545ea1f..887dc60b1a 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -72,18 +72,6 @@ const expectedEntities: Entity[] = [ owner: 'someone', }, }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'test-entity', - description: 'The expected description', - }, - spec: { - type: 'some-type', - lifecycle: 'experimental', - }, - }, ]; describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { @@ -198,8 +186,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}`, }, @@ -208,8 +196,13 @@ describe('DefaultTechDocsCollator', () => { }); it('maps a returned entity with a custom locationTemplate', async () => { + const mockConfig = new ConfigReader({ + techdocs: { + legacyUseCaseSensitiveTripletPaths: true, + }, + }); // Provide an alternate location template. - collator = new DefaultTechDocsCollator({ + collator = DefaultTechDocsCollator.fromConfig(mockConfig, { discovery: mockDiscoveryApi, tokenManager: mockTokenManager, locationTemplate: '/software/:name', diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index 5149fe6c09..d36e1c0ae5 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -20,17 +20,21 @@ import { } from '@backstage/backend-common'; import { Entity, + parseEntityRef, RELATION_OWNED_BY, stringifyEntityRef, } from '@backstage/catalog-model'; -import { DocumentCollator } from '@backstage/search-common'; import fetch from 'node-fetch'; import unescape from 'lodash/unescape'; import { Logger } from 'winston'; import pLimit from 'p-limit'; import { Config } from '@backstage/config'; import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; -import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; +import { + CatalogApi, + CatalogClient, + CATALOG_FILTER_EXISTS, +} from '@backstage/catalog-client'; import { TechDocsDocument } from '@backstage/techdocs-common'; interface MkSearchIndexDoc { @@ -39,6 +43,11 @@ interface MkSearchIndexDoc { location: string; } +/** + * Options to configure the TechDocs collator + * + * @public + */ export type TechDocsCollatorOptions = { discovery: PluginEndpointDiscovery; logger: Logger; @@ -55,47 +64,50 @@ type EntityInfo = { kind: string; }; -export class DefaultTechDocsCollator implements DocumentCollator { - protected discovery: PluginEndpointDiscovery; - protected locationTemplate: string; - private readonly logger: Logger; - private readonly catalogClient: CatalogApi; - private readonly tokenManager: TokenManager; - private readonly parallelismLimit: number; - private readonly legacyPathCasing: boolean; +/** + * A search collator responsible for gathering and transforming TechDocs documents. + * + * @public + * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and + * use `DefaultTechDocsCollatorFactory` instead. + */ +export class DefaultTechDocsCollator { public readonly type: string = 'techdocs'; public readonly visibilityPermission = catalogEntityReadPermission; - /** - * @deprecated use static fromConfig method instead. - */ - constructor(options: TechDocsCollatorOptions) { - this.discovery = options.discovery; - this.locationTemplate = - options.locationTemplate || '/docs/:namespace/:kind/:name/:path'; - this.logger = options.logger; - this.catalogClient = - options.catalogClient || - new CatalogClient({ discoveryApi: options.discovery }); - this.parallelismLimit = options.parallelismLimit ?? 10; - this.legacyPathCasing = options.legacyPathCasing ?? false; - this.tokenManager = options.tokenManager; - } + private constructor( + private readonly legacyPathCasing: boolean, + private readonly options: TechDocsCollatorOptions, + ) {} static fromConfig(config: Config, options: TechDocsCollatorOptions) { const legacyPathCasing = config.getOptionalBoolean( 'techdocs.legacyUseCaseSensitiveTripletPaths', ) || false; - return new DefaultTechDocsCollator({ ...options, legacyPathCasing }); + return new DefaultTechDocsCollator(legacyPathCasing, options); } async execute() { - const limit = pLimit(this.parallelismLimit); - const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs'); - const { token } = await this.tokenManager.getToken(); - const entities = await this.catalogClient.getEntities( + const { + parallelismLimit, + discovery, + tokenManager, + catalogClient, + locationTemplate, + logger, + } = this.options; + const limit = pLimit(parallelismLimit ?? 10); + const techDocsBaseUrl = await discovery.getBaseUrl('techdocs'); + const { token } = await tokenManager.getToken(); + const entities = await ( + catalogClient ?? new CatalogClient({ discoveryApi: discovery }) + ).getEntities( { + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, fields: [ 'kind', 'namespace', @@ -110,63 +122,60 @@ export class DefaultTechDocsCollator implements DocumentCollator { }, { token }, ); - const docPromises = entities.items - .filter(it => it.metadata?.annotations?.['backstage.io/techdocs-ref']) - .map((entity: Entity) => - limit(async (): Promise => { - const entityInfo = DefaultTechDocsCollator.handleEntityInfoCasing( - this.legacyPathCasing, + const docPromises = entities.items.map((entity: Entity) => + limit(async (): Promise => { + const entityInfo = DefaultTechDocsCollator.handleEntityInfoCasing( + this.legacyPathCasing ?? false, + { + kind: entity.kind, + namespace: entity.metadata.namespace || 'default', + name: entity.metadata.name, + }, + ); + + try { + const searchIndexResponse = await fetch( + DefaultTechDocsCollator.constructDocsIndexUrl( + techDocsBaseUrl, + entityInfo, + ), { - kind: entity.kind, - namespace: entity.metadata.namespace || 'default', - name: entity.metadata.name, + headers: { + Authorization: `Bearer ${token}`, + }, }, ); + const searchIndex = await searchIndexResponse.json(); - try { - const searchIndexResponse = await fetch( - DefaultTechDocsCollator.constructDocsIndexUrl( - techDocsBaseUrl, - entityInfo, - ), + return searchIndex.docs.map((doc: MkSearchIndexDoc) => ({ + title: unescape(doc.title), + text: unescape(doc.text || ''), + location: this.applyArgsToFormat( + locationTemplate || '/docs/:namespace/:kind/:name/:path', { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); - const searchIndex = await searchIndexResponse.json(); - - return searchIndex.docs.map((doc: MkSearchIndexDoc) => ({ - title: unescape(doc.title), - text: unescape(doc.text || ''), - location: this.applyArgsToFormat(this.locationTemplate, { ...entityInfo, path: doc.location, - }), - path: doc.location, - kind: entity.kind, - namespace: entity.metadata.namespace || 'default', - name: entity.metadata.name, - entityTitle: entity.metadata.title, - componentType: entity.spec?.type?.toString() || 'other', - lifecycle: (entity.spec?.lifecycle as string) || '', - owner: - entity.relations?.find(r => r.type === RELATION_OWNED_BY) - ?.target?.name || '', - authorization: { - resourceRef: stringifyEntityRef(entity), }, - })); - } catch (e) { - this.logger.debug( - `Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`, - e, - ); - return []; - } - }), - ); + ), + path: doc.location, + ...entityInfo, + entityTitle: entity.metadata.title, + componentType: entity.spec?.type?.toString() || 'other', + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: getSimpleEntityOwnerString(entity), + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + })); + } catch (e) { + logger.debug( + `Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`, + e, + ); + return []; + } + }), + ); return (await Promise.all(docPromises)).flat(); } @@ -199,3 +208,14 @@ export class DefaultTechDocsCollator implements DocumentCollator { }, {} as EntityInfo); } } + +function getSimpleEntityOwnerString(entity: Entity): string { + if (entity.relations) { + const owner = entity.relations.find(r => r.type === RELATION_OWNED_BY); + if (owner) { + const { name } = parseEntityRef(owner.targetRef); + return name; + } + } + return ''; +} diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts new file mode 100644 index 0000000000..fafaae71b9 --- /dev/null +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts @@ -0,0 +1,245 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + getVoidLogger, + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { Readable } from 'stream'; +import { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory'; + +const logger = getVoidLogger(); + +const mockSearchDocIndex = { + config: { + lang: ['en'], + min_search_length: 3, + prebuild_index: false, + separator: '[\\s\\-]+', + }, + docs: [ + { + location: '', + text: 'docs docs docs', + title: 'Home', + }, + { + location: 'local-development/', + text: 'Docs for first subtitle', + title: 'Local development', + }, + { + location: 'local-development/#development', + text: 'Docs for sub-subtitle', + title: 'Development', + }, + ], +}; + +const expectedEntities: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity with Docs!', + name: 'test-entity-with-docs', + description: 'Documented description', + annotations: { + 'backstage.io/techdocs-ref': './', + }, + }, + spec: { + type: 'dog', + lifecycle: 'experimental', + owner: 'someone', + }, + }, +]; + +describe('DefaultTechDocsCollatorFactory', () => { + const config = new ConfigReader({}); + const mockDiscoveryApi: jest.Mocked = { + getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'), + getExternalBaseUrl: jest.fn(), + }; + const mockTokenManager: jest.Mocked = { + getToken: jest.fn().mockResolvedValue({ token: '' }), + authenticate: jest.fn(), + }; + const options = { + discovery: mockDiscoveryApi, + logger: getVoidLogger(), + tokenManager: mockTokenManager, + }; + + it('has expected type', () => { + const factory = DefaultTechDocsCollatorFactory.fromConfig(config, options); + expect(factory.type).toBe('techdocs'); + }); + + describe('getCollator', () => { + let factory: DefaultTechDocsCollatorFactory; + let collator: Readable; + + const worker = setupServer(); + setupRequestMockHandlers(worker); + + beforeEach(async () => { + factory = DefaultTechDocsCollatorFactory.fromConfig(config, options); + collator = await factory.getCollator(); + + worker.use( + rest.get( + 'http://test-backend/static/docs/default/Component/test-entity-with-docs/search/search_index.json', + (_, res, ctx) => res(ctx.status(200), ctx.json(mockSearchDocIndex)), + ), + rest.get('http://test-backend/entities', (req, res, ctx) => { + // Imitate offset/limit pagination. + const offset = parseInt( + req.url.searchParams.get('offset') || '0', + 10, + ); + const limit = parseInt( + req.url.searchParams.get('limit') || '500', + 10, + ); + + // Limit 50 corresponds to a case testing pagination. + if (limit === 50) { + // Return 50 copies of invalid entities on the first request. + if (offset === 0) { + return res(ctx.status(200), ctx.json(Array(50).fill({}))); + } + // Then just the regular 2 on the second. + return res(ctx.status(200), ctx.json(expectedEntities)); + } + return res( + ctx.status(200), + ctx.json(expectedEntities.slice(offset, limit + offset)), + ); + }), + ); + }); + + it('returns a readable stream', async () => { + expect(collator).toBeInstanceOf(Readable); + }); + + it('fetches from the configured catalog and tech docs services', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog'); + expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('techdocs'); + expect(documents).toHaveLength(mockSearchDocIndex.docs.length); + }); + + it('should create documents for each tech docs search index', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + const entity = expectedEntities[0]; + documents.forEach((document, idx) => { + expect(document).toMatchObject({ + title: mockSearchDocIndex.docs[idx].title, + location: `/docs/default/component/${entity.metadata.name}/${mockSearchDocIndex.docs[idx].location}`, + text: mockSearchDocIndex.docs[idx].text, + namespace: 'default', + entityTitle: entity!.metadata.title, + componentType: entity!.spec!.type, + lifecycle: entity!.spec!.lifecycle, + owner: '', + kind: entity.kind.toLocaleLowerCase('en-US'), + name: entity.metadata.name, + }); + }); + }); + + it('maps a returned entity with a custom locationTemplate', async () => { + // Provide an alternate location template. + factory = DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + locationTemplate: '/software/:name', + logger, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents[0]).toMatchObject({ + location: '/software/test-entity-with-docs', + }); + }); + + it('paginates through catalog entities using batchSize', async () => { + // A parallelismLimit of 1 is a catalog limit of 50 per request. Code + // above in the /entities handler ensures valid entities are only + // returned on the second page. + factory = DefaultTechDocsCollatorFactory.fromConfig(config, { + ...options, + parallelismLimit: 1, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + // Only 1 entity with TechDocs configured multipled by 3 pages. + expect(documents).toHaveLength(3); + }); + + describe('with legacyPathCasing configuration', () => { + beforeEach(async () => { + const legacyConfig = new ConfigReader({ + techdocs: { + legacyUseCaseSensitiveTripletPaths: true, + }, + }); + factory = DefaultTechDocsCollatorFactory.fromConfig( + legacyConfig, + options, + ); + collator = await factory.getCollator(); + }); + + it('should create documents for each tech docs search index', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + const entity = expectedEntities[0]; + documents.forEach((document, idx) => { + expect(document).toMatchObject({ + title: mockSearchDocIndex.docs[idx].title, + location: `/docs/default/Component/${entity.metadata.name}/${mockSearchDocIndex.docs[idx].location}`, + text: mockSearchDocIndex.docs[idx].text, + namespace: 'default', + entityTitle: entity!.metadata.title, + componentType: entity!.spec!.type, + lifecycle: entity!.spec!.lifecycle, + owner: '', + kind: entity.kind, + name: entity.metadata.name, + }); + }); + }); + }); + }); +}); diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts new file mode 100644 index 0000000000..181f032b54 --- /dev/null +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts @@ -0,0 +1,254 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + CATALOG_FILTER_EXISTS, +} from '@backstage/catalog-client'; +import { + Entity, + parseEntityRef, + RELATION_OWNED_BY, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; +import { DocumentCollatorFactory } from '@backstage/search-common'; +import { TechDocsDocument } from '@backstage/techdocs-common'; +import unescape from 'lodash/unescape'; +import fetch from 'node-fetch'; +import pLimit from 'p-limit'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; + +interface MkSearchIndexDoc { + title: string; + text: string; + location: string; +} + +/** + * Options to configure the TechDocs collator factory + * + * @public + */ +export type TechDocsCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger; + tokenManager: TokenManager; + locationTemplate?: string; + catalogClient?: CatalogApi; + parallelismLimit?: number; + legacyPathCasing?: boolean; +}; + +type EntityInfo = { + name: string; + namespace: string; + kind: string; +}; + +/** + * A search collator factory responsible for gathering and transforming + * TechDocs documents. + * + * @public + */ +export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'techdocs'; + public readonly visibilityPermission = catalogEntityReadPermission; + + private discovery: PluginEndpointDiscovery; + private locationTemplate: string; + private readonly logger: Logger; + private readonly catalogClient: CatalogApi; + private readonly tokenManager: TokenManager; + private readonly parallelismLimit: number; + private readonly legacyPathCasing: boolean; + + private constructor(options: TechDocsCollatorFactoryOptions) { + this.discovery = options.discovery; + this.locationTemplate = + options.locationTemplate || '/docs/:namespace/:kind/:name/:path'; + this.logger = options.logger; + this.catalogClient = + options.catalogClient || + new CatalogClient({ discoveryApi: options.discovery }); + this.parallelismLimit = options.parallelismLimit ?? 10; + this.legacyPathCasing = options.legacyPathCasing ?? false; + this.tokenManager = options.tokenManager; + } + + static fromConfig(config: Config, options: TechDocsCollatorFactoryOptions) { + const legacyPathCasing = + config.getOptionalBoolean( + 'techdocs.legacyUseCaseSensitiveTripletPaths', + ) || false; + return new DefaultTechDocsCollatorFactory({ ...options, legacyPathCasing }); + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private async *execute(): AsyncGenerator { + const limit = pLimit(this.parallelismLimit); + const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs'); + const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; + let moreEntitiesToGet = true; + + // Offset/limit pagination is used on the Catalog Client in order to + // limit (and allow some control over) memory used by the search backend + // at index-time. The batchSize is calculated as a factor of the given + // parallelism limit to simplify configuration. + const batchSize = this.parallelismLimit * 50; + while (moreEntitiesToGet) { + const entities = ( + await this.catalogClient.getEntities( + { + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, + fields: [ + 'kind', + 'namespace', + 'metadata.annotations', + 'metadata.name', + 'metadata.title', + 'metadata.namespace', + 'spec.type', + 'spec.lifecycle', + 'relations', + ], + limit: batchSize, + offset: entitiesRetrieved, + }, + { token }, + ) + ).items; + + // Control looping through entity batches. + moreEntitiesToGet = entities.length === batchSize; + entitiesRetrieved += entities.length; + + const docPromises = entities + .filter(it => it.metadata?.annotations?.['backstage.io/techdocs-ref']) + .map((entity: Entity) => + limit(async (): Promise => { + const entityInfo = + DefaultTechDocsCollatorFactory.handleEntityInfoCasing( + this.legacyPathCasing, + { + kind: entity.kind, + namespace: entity.metadata.namespace || 'default', + name: entity.metadata.name, + }, + ); + + try { + const searchIndexResponse = await fetch( + DefaultTechDocsCollatorFactory.constructDocsIndexUrl( + techDocsBaseUrl, + entityInfo, + ), + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + const searchIndex = await searchIndexResponse.json(); + + return searchIndex.docs.map((doc: MkSearchIndexDoc) => ({ + title: unescape(doc.title), + text: unescape(doc.text || ''), + location: this.applyArgsToFormat( + this.locationTemplate || '/docs/:namespace/:kind/:name/:path', + { + ...entityInfo, + path: doc.location, + }, + ), + path: doc.location, + ...entityInfo, + entityTitle: entity.metadata.title, + componentType: entity.spec?.type?.toString() || 'other', + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: getSimpleEntityOwnerString(entity), + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + })); + } catch (e) { + this.logger.debug( + `Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`, + e, + ); + return []; + } + }), + ); + yield* (await Promise.all(docPromises)).flat(); + } + } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + return formatted; + } + + private static constructDocsIndexUrl( + techDocsBaseUrl: string, + entityInfo: { kind: string; namespace: string; name: string }, + ) { + return `${techDocsBaseUrl}/static/docs/${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}/search/search_index.json`; + } + + private static handleEntityInfoCasing( + legacyPaths: boolean, + entityInfo: EntityInfo, + ): EntityInfo { + return legacyPaths + ? entityInfo + : Object.entries(entityInfo).reduce((acc, [key, value]) => { + return { ...acc, [key]: value.toLocaleLowerCase('en-US') }; + }, {} as EntityInfo); + } +} + +function getSimpleEntityOwnerString(entity: Entity): string { + if (entity.relations) { + const owner = entity.relations.find(r => r.type === RELATION_OWNED_BY); + if (owner) { + const { name } = parseEntityRef(owner.targetRef); + return name; + } + } + return ''; +} diff --git a/plugins/techdocs-backend/src/search/index.ts b/plugins/techdocs-backend/src/search/index.ts index fbbd23b964..68e3b4edc7 100644 --- a/plugins/techdocs-backend/src/search/index.ts +++ b/plugins/techdocs-backend/src/search/index.ts @@ -13,6 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; +export { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory'; +export type { TechDocsCollatorFactoryOptions } from './DefaultTechDocsCollatorFactory'; + +/** + * todo(backstage/techdocs-core): stop exporting these in a future release. + */ +export { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; export type { TechDocsCollatorOptions } from './DefaultTechDocsCollator'; diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts new file mode 100644 index 0000000000..84bd960f8f --- /dev/null +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DefaultDocsBuildStrategy } from './DocsBuildStrategy'; +import { ConfigReader } from '@backstage/config'; + +const MockedConfigReader = ConfigReader as jest.MockedClass< + typeof ConfigReader +>; + +jest.mock('@backstage/config'); + +describe('DefaultDocsBuildStrategy', () => { + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + uid: '0', + name: 'test', + }, + }; + + const config = new ConfigReader({}); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('shouldBuild', () => { + it('should return true when techdocs.build is set to local', async () => { + const defaultDocsBuildStrategy = + DefaultDocsBuildStrategy.fromConfig(config); + + MockedConfigReader.prototype.getString.mockReturnValue('local'); + + const result = await defaultDocsBuildStrategy.shouldBuild({ entity }); + + expect(result).toBe(true); + }); + + it('should return false when techdocs.build is set to external', async () => { + const defaultDocsBuildStrategy = + DefaultDocsBuildStrategy.fromConfig(config); + + MockedConfigReader.prototype.getString.mockReturnValue('external'); + + const result = await defaultDocsBuildStrategy.shouldBuild({ entity }); + + expect(result).toBe(false); + }); + }); +}); diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts new file mode 100644 index 0000000000..42a16234f9 --- /dev/null +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; + +/** + * Parameters passed to the shouldBuild method on the DocsBuildStrategy interface + * + * @public + */ +export type ShouldBuildParameters = { + entity: Entity; +}; + +/** + * A strategy for when to build TechDocs locally, and when to skip building TechDocs (allowing for an external build) + * + * @public + */ +export interface DocsBuildStrategy { + shouldBuild(params: ShouldBuildParameters): Promise; +} + +export class DefaultDocsBuildStrategy { + private readonly config: Config; + + private constructor(config: Config) { + this.config = config; + } + + static fromConfig(config: Config): DefaultDocsBuildStrategy { + return new DefaultDocsBuildStrategy(config); + } + + async shouldBuild(_: ShouldBuildParameters): Promise { + return this.config.getString('techdocs.builder') === 'local'; + } +} diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 099eec152b..2979d2efe4 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -15,7 +15,7 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, NotFoundError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -177,7 +177,7 @@ export class DocsSynchronizer { // Fetch techdocs_metadata.json from the publisher and from cache. const baseUrl = await discovery.getBaseUrl('techdocs'); - const namespace = entity.metadata?.namespace || ENTITY_DEFAULT_NAMESPACE; + const namespace = entity.metadata?.namespace || DEFAULT_NAMESPACE; const kind = entity.kind; const name = entity.metadata.name; const legacyPathCasing = diff --git a/plugins/techdocs-backend/src/service/index.ts b/plugins/techdocs-backend/src/service/index.ts index 0065e33a2a..7355a34e32 100644 --- a/plugins/techdocs-backend/src/service/index.ts +++ b/plugins/techdocs-backend/src/service/index.ts @@ -20,3 +20,7 @@ export type { RecommendedDeploymentOptions, OutOfTheBoxDeploymentOptions, } from './router'; +export type { + DocsBuildStrategy, + ShouldBuildParameters, +} from './DocsBuildStrategy'; diff --git a/plugins/techdocs-backend/src/service/router.test.ts b/plugins/techdocs-backend/src/service/router.test.ts index 43e9641efb..c7cda203bb 100644 --- a/plugins/techdocs-backend/src/service/router.test.ts +++ b/plugins/techdocs-backend/src/service/router.test.ts @@ -38,6 +38,7 @@ import { RouterOptions, } from './router'; import { TechDocsCache } from '../cache'; +import { DocsBuildStrategy } from './DocsBuildStrategy'; jest.mock('@backstage/catalog-client'); jest.mock('@backstage/config'); @@ -120,6 +121,9 @@ describe('createRouter', () => { const cache: jest.Mocked = { getClient: jest.fn(), }; + const docsBuildStrategy: jest.Mocked = { + shouldBuild: jest.fn(), + }; const outOfTheBoxOptions = { preparers, generators, @@ -128,6 +132,7 @@ describe('createRouter', () => { logger: getVoidLogger(), discovery, cache, + docsBuildStrategy, }; const recommendedOptions = { publisher, @@ -135,6 +140,7 @@ describe('createRouter', () => { logger: getVoidLogger(), discovery, cache, + docsBuildStrategy, }; beforeEach(() => { @@ -181,10 +187,10 @@ describe('createRouter', () => { expect(response.status).toBe(404); }); - it('should not check for an update without local builder', async () => { + it('should not check for an update when shouldBuild returns false', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('external'); + docsBuildStrategy.shouldBuild.mockResolvedValue(false); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doCacheSync.mockImplementation( async ({ responseHandler }) => @@ -198,10 +204,10 @@ describe('createRouter', () => { expect(response.status).toBe(304); }); - it('should error if missing builder', async () => { + it('should error if build is required and is missing preparer', async () => { const app = await createApp(recommendedOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); const response = await request(app) @@ -210,7 +216,7 @@ describe('createRouter', () => { expect(response.status).toBe(500); expect(response.text).toMatch( - /Invalid configuration\. 'techdocs\.builder' was set to 'local' but no 'preparer' was provided to the router initialization/, + /Invalid configuration\. docsBuildStrategy\.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization./, ); expect(MockDocsSynchronizer.prototype.doSync).toBeCalledTimes(0); @@ -219,7 +225,7 @@ describe('createRouter', () => { it('should execute synchronization', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => @@ -244,7 +250,7 @@ describe('createRouter', () => { it('should return on updated', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => { @@ -297,10 +303,10 @@ describe('createRouter', () => { expect(response.status).toBe(404); }); - it('should not check for an update without local builder', async () => { + it('should not check for an update when shouldBuild returns false', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('external'); + docsBuildStrategy.shouldBuild.mockResolvedValue(false); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doCacheSync.mockImplementation( async ({ responseHandler }) => @@ -322,10 +328,10 @@ data: {"updated":false} ); }); - it('should error if missing builder', async () => { + it('should error if build is required and is missing preparer', async () => { const app = await createApp(recommendedOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); const response = await request(app) @@ -337,7 +343,7 @@ data: {"updated":false} expect(response.get('content-type')).toBe('text/event-stream'); expect(response.text).toEqual( `event: error -data: "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization." +data: "Invalid configuration. docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization." `, ); @@ -348,7 +354,7 @@ data: "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'prep it('should execute synchronization', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => @@ -376,7 +382,7 @@ data: "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'prep it('should return an event-stream', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => { diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 141ce93f79..73f212d049 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -35,10 +35,16 @@ import { ScmIntegrations } from '@backstage/integration'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; import { createCacheMiddleware, TechDocsCache } from '../cache'; import { CachedEntityLoader } from './CachedEntityLoader'; +import { + DefaultDocsBuildStrategy, + DocsBuildStrategy, +} from './DocsBuildStrategy'; /** - * All of the required dependencies for running TechDocs in the "out-of-the-box" + * Required dependencies for running TechDocs in the "out-of-the-box" * deployment configuration (prepare/generate/publish all in the Backend). + * + * @public */ export type OutOfTheBoxDeploymentOptions = { preparers: PreparerBuilder; @@ -49,11 +55,14 @@ export type OutOfTheBoxDeploymentOptions = { database?: Knex; // TODO: Make database required when we're implementing database stuff. config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; /** * Required dependencies for running TechDocs in the "recommended" deployment * configuration (prepare/generate handled externally in CI/CD). + * + * @public */ export type RecommendedDeploymentOptions = { publisher: PublisherBase; @@ -61,10 +70,13 @@ export type RecommendedDeploymentOptions = { discovery: PluginEndpointDiscovery; config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; /** * One of the two deployment configurations must be provided. + * + * @public */ export type RouterOptions = | RecommendedDeploymentOptions @@ -73,6 +85,8 @@ export type RouterOptions = /** * Typeguard to help createRouter() understand when we are in a "recommended" * deployment vs. when we are in an out-of-the-box deployment configuration. + * + * * @public */ function isOutOfTheBoxOption( opt: RouterOptions, @@ -80,12 +94,19 @@ function isOutOfTheBoxOption( return (opt as OutOfTheBoxDeploymentOptions).preparers !== undefined; } +/** + * Creates a techdocs router. + * + * @public + */ export async function createRouter( options: RouterOptions, ): Promise { const router = Router(); const { publisher, config, logger, discovery } = options; const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const docsBuildStrategy = + options.docsBuildStrategy ?? DefaultDocsBuildStrategy.fromConfig(config); // Entities are cached to optimize the /static/docs request path, which can be called many times // when loading a single techdocs page. @@ -197,10 +218,13 @@ export async function createRouter( responseHandler = createEventStream(res); } - // techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to 'local' - // If set to 'external', it will assume that an external process (e.g. CI/CD pipeline - // of the repository) is responsible for building and publishing documentation to the storage provider - if (config.getString('techdocs.builder') !== 'local') { + // By default, techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to + // 'local'. If set to 'external', it will assume that an external process (e.g. CI/CD pipeline + // of the repository) is responsible for building and publishing documentation to the storage provider. + // Altering the implementation of the injected docsBuildStrategy allows for more complex behaviours, based on + // either config or the properties of the entity (e.g. annotations, labels, spec fields etc.). + const shouldBuild = await docsBuildStrategy.shouldBuild({ entity }); + if (!shouldBuild) { // However, if caching is enabled, take the opportunity to check and // invalidate stale cache entries. if (cache) { @@ -231,7 +255,7 @@ export async function createRouter( responseHandler.error( new Error( - "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization.", + "Invalid configuration. docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization.", ), ); }); @@ -323,12 +347,8 @@ export function createEventStream( } /** - * Create a HTTP response. This is used for the legacy non-event-stream implementation of the sync endpoint. - * - * @param res - the response to write the event-stream to - * @returns A tuple of callbacks to emit messages. A call to 'error' or 'finish' - * will close the event-stream. - */ + * @deprecated use event-stream implementation of the sync endpoint + * */ export function createHttpResponse( res: Response, ): DocsSynchronizerSyncOpts { diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 3ff8c0e175..97104ea5a4 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-techdocs +## 0.14.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. +- 4faae902eb: Adjust the Tech Docs page theme as a side effect of the `mkdocs-material` theme update. + + If you use the `spofify/techdocs` image to build your documentation, make sure you use version `spotify/techdocs:v0.3.7`. + + **Breaking**: The `PyMdown` extensions have also been updated and some syntax may have changed, so it is recommended that you check the extension's documentation if something stops working. + For example, the syntax of tags below was deprecated in `PyMdown` extensions `v.7.0` and in `v.8.0.0` it has been removed. This means that the old syntax specified below no longer works. + + ````markdown + ```markdown tab="tab" + This is some markdown + ``` + + ```markdown tab="tab 2" + This is some markdown in tab 2 + ``` + ```` + +### Patch Changes + +- 3bbb4d98c6: Changed to use from createApp +- ed09ad8093: Updated usage of the `LocationSpec` type from `@backstage/catalog-model`, which is deprecated. +- b776ce5aab: Replaced use of deprecated `useEntityListProvider` hook with `useEntityList`. +- d4f67fa728: Removed import of deprecated hook. +- 45e1706328: Continuation of [#9569](https://github.com/backstage/backstage/pull/9569), fix Tech Docs Reader search position to be the same width as content. +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/plugin-catalog@0.9.0 + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration@0.7.5 + - @backstage/plugin-search@0.7.1 + - @backstage/integration-react@0.1.23 + +## 0.13.4 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 6553985cd4: Match text size of admonitions to main content text size. +- 9df7b43e1a: Improve overall appearance of highlighted code in docs. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/integration-react@0.1.22 + - @backstage/plugin-catalog@0.8.0 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/plugin-search@0.7.0 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + ## 0.13.3 ### Patch Changes diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 9a25cb6375..917c1d5d85 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -5,7 +5,6 @@ ```ts /// -import { Action } from '@material-table/core'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Config } from '@backstage/config'; @@ -15,122 +14,76 @@ import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; -import { LocationSpec } from '@backstage/catalog-model'; +import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; -// Warning: (ae-missing-release-tag) "createCopyDocsUrlAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -function createCopyDocsUrlAction(copyToClipboard: Function): ( - row: DocsTableRow, -) => { - icon: () => JSX.Element; - tooltip: string; - onClick: () => any; -}; +// @public +export const DefaultTechDocsHome: ( + props: DefaultTechDocsHomeProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "createNameColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -function createNameColumn(): 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) "createStarEntityAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -function createStarEntityAction( - isStarredEntity: Function, - toggleStarredEntity: Function, -): ({ entity }: DocsTableRow) => { - cellStyle: { - paddingLeft: string; - }; - icon: () => JSX.Element; - tooltip: string; - onClick: () => any; -}; - -// Warning: (ae-missing-release-tag) "createTypeColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -function createTypeColumn(): TableColumn; - -// Warning: (ae-missing-release-tag) "DefaultTechDocsHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const DefaultTechDocsHome: ({ - initialFilter, - columns, - actions, -}: { - initialFilter?: UserListFilterKind | undefined; - columns?: TableColumn[] | undefined; +// @public +export type DefaultTechDocsHomeProps = { + initialFilter?: UserListFilterKind; + columns?: TableColumn[]; actions?: TableProps['actions']; -}) => JSX.Element; +}; -// Warning: (ae-missing-release-tag) "DocsCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const DocsCardGrid: ({ - entities, -}: { +// @public +export const DocsCardGrid: (props: DocsCardGridProps) => JSX.Element | null; + +// @public +export type DocsCardGridProps = { entities: Entity[] | undefined; -}) => JSX.Element | null; +}; -// Warning: (ae-missing-release-tag) "DocsResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const DocsResultListItem: ({ - result, - lineClamp, - asListItem, - asLink, - title, -}: { - result: any; - lineClamp?: number | undefined; - asListItem?: boolean | undefined; - asLink?: boolean | undefined; - title?: string | undefined; -}) => JSX.Element; +// @public @deprecated (undocumented) +export const DocsResultListItem: ( + props: TechDocsSearchResultListItemProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "DocsTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const DocsTable: ({ - entities, - title, - loading, - columns, - actions, -}: { +// @public +export const DocsTable: { + (props: DocsTableProps): JSX.Element | null; + columns: { + createNameColumn(): TableColumn; + createOwnerColumn(): TableColumn; + createTypeColumn(): TableColumn; + }; + actions: { + createCopyDocsUrlAction(copyToClipboard: Function): (row: DocsTableRow) => { + icon: () => JSX.Element; + tooltip: string; + onClick: () => any; + }; + createStarEntityAction( + isStarredEntity: Function, + toggleStarredEntity: Function, + ): ({ entity }: DocsTableRow) => { + cellStyle: { + paddingLeft: string; + }; + icon: () => JSX.Element; + tooltip: string; + onClick: () => any; + }; + }; +}; + +// @public +export type DocsTableProps = { entities: Entity[] | undefined; title?: string | undefined; loading?: boolean | undefined; - columns?: TableColumn[] | undefined; - actions?: - | ( - | Action - | { - action: (rowData: DocsTableRow) => Action; - position: string; - } - | ((rowData: DocsTableRow) => Action) - )[] - | undefined; -}) => JSX.Element | null; + columns?: TableColumn[]; + actions?: TableProps['actions']; +}; -// Warning: (ae-missing-release-tag) "DocsTableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type DocsTableRow = { entity: Entity; resolved: { @@ -140,76 +93,101 @@ export type DocsTableRow = { }; }; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EmbeddedDocsRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EmbeddedDocsRouter: (_props: Props_2) => JSX.Element; +// @public +export const EmbeddedDocsRouter: () => JSX.Element; -// Warning: (ae-missing-release-tag) "EntityListDocsGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const EntityListDocsGrid: () => JSX.Element; -// Warning: (ae-missing-release-tag) "EntityListDocsTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const EntityListDocsTable: { - ({ - columns, - actions, - }: { - columns?: TableColumn[] | undefined; - actions?: TableProps['actions']; - }): JSX.Element; - columns: typeof columnFactories; - actions: typeof actionFactories; + (props: EntityListDocsTableProps): JSX.Element; + columns: { + createNameColumn(): TableColumn; + createOwnerColumn(): TableColumn; + createTypeColumn(): TableColumn; + }; + actions: { + createCopyDocsUrlAction(copyToClipboard: Function): (row: DocsTableRow) => { + icon: () => JSX.Element; + tooltip: string; + onClick: () => any; + }; + createStarEntityAction( + isStarredEntity: Function, + toggleStarredEntity: Function, + ): ({ entity }: DocsTableRow) => { + cellStyle: { + paddingLeft: string; + }; + icon: () => JSX.Element; + tooltip: string; + onClick: () => any; + }; + }; }; -// Warning: (ae-missing-release-tag) "EntityTechdocsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EntityTechdocsContent: (_props: { - entity?: Entity | undefined; -}) => JSX.Element; +// @public +export type EntityListDocsTableProps = { + columns?: TableColumn[]; + actions?: TableProps['actions']; +}; -// Warning: (ae-missing-release-tag) "isTechDocsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export const EntityTechdocsContent: () => JSX.Element; + +// @public export const isTechDocsAvailable: (entity: Entity) => boolean; -// Warning: (ae-missing-release-tag) "PanelType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export interface PanelConfig { + // (undocumented) + description: string; + // (undocumented) + filterPredicate: ((entity: Entity) => boolean) | string; + // (undocumented) + panelCSS?: CSSProperties; + // (undocumented) + panelType: PanelType; + // (undocumented) + title: string; +} + +// @public export type PanelType = 'DocsCardGrid' | 'DocsTable'; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "Reader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const Reader: ({ - entityRef, - onReady, - withSearch, -}: Props_3) => JSX.Element; +// @public +export const Reader: (props: ReaderProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type ReaderProps = { + entityRef: EntityName; + withSearch?: boolean; + onReady?: () => void; +}; + +// @public export const Router: () => JSX.Element; // @public export type SyncResult = 'cached' | 'updated'; +// @public +export interface TabConfig { + // (undocumented) + label: string; + // (undocumented) + panels: PanelConfig[]; +} + +// @public +export type TabsConfig = TabConfig[]; + // @public export interface TechDocsApi { getApiOrigin(): Promise; - // Warning: (ae-forgotten-export) The symbol "TechDocsEntityMetadata" needs to be exported by the entry point index.d.ts - // // (undocumented) getEntityMetadata(entityId: EntityName): Promise; - // Warning: (ae-forgotten-export) The symbol "TechDocsMetadata" needs to be exported by the entry point index.d.ts - // // (undocumented) getTechDocsMetadata(entityId: EntityName): Promise; } @@ -234,83 +212,64 @@ export class TechDocsClient implements TechDocsApi { getTechDocsMetadata(entityId: EntityName): Promise; } -// Warning: (ae-missing-release-tag) "TechDocsCustomHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const TechDocsCustomHome: ({ - tabsConfig, -}: { - tabsConfig: TabsConfig; -}) => JSX.Element; +// @public +export const TechDocsCustomHome: ( + props: TechDocsCustomHomeProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "TechDocsIndexPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type TechDocsCustomHomeProps = { + tabsConfig: TabsConfig; +}; + +// @public +export type TechDocsEntityMetadata = Entity & { + locationMetadata?: { + type: string; + target: string; + }; +}; + +// @public export const TechDocsIndexPage: () => JSX.Element; -// Warning: (ae-missing-release-tag) "TechDocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const TechDocsPage: ({ children }: TechDocsPageProps) => JSX.Element; +// @public +export type TechDocsMetadata = { + site_name: string; + site_description: string; +}; -// Warning: (ae-missing-release-tag) "TechdocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) +export const TechDocsPage: (props: TechDocsReaderPageProps) => JSX.Element; + +// @public export const TechdocsPage: () => JSX.Element; -// Warning: (ae-missing-release-tag) "TechDocsPageHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const TechDocsPageHeader: ({ - entityRef, - entityMetadata, - techDocsMetadata, -}: TechDocsPageHeaderProps) => JSX.Element; +// @public @deprecated (undocumented) +export const TechDocsPageHeader: ( + props: TechDocsReaderPageHeaderProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "TechDocsPageHeaderProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type TechDocsPageHeaderProps = { - entityRef: EntityName; - entityMetadata?: TechDocsEntityMetadata; - techDocsMetadata?: TechDocsMetadata; +// @public @deprecated (undocumented) +export type TechDocsPageHeaderProps = TechDocsReaderPageHeaderProps; + +// @public @deprecated (undocumented) +export type TechDocsPageRenderFunction = TechDocsReaderPageRenderFunction; + +// @public +export const TechDocsPageWrapper: ( + props: TechDocsPageWrapperProps, +) => JSX.Element; + +// @public +export type TechDocsPageWrapperProps = { + children?: React_2.ReactNode; }; -// Warning: (ae-missing-release-tag) "TechDocsPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type TechDocsPageProps = { - children?: TechDocsPageRenderFunction | React_2.ReactNode; -}; - -// Warning: (ae-missing-release-tag) "TechDocsPageRenderFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type TechDocsPageRenderFunction = ({ - techdocsMetadataValue, - entityMetadataValue, - entityRef, -}: { - techdocsMetadataValue?: TechDocsMetadata | undefined; - entityMetadataValue?: TechDocsEntityMetadata | undefined; - entityRef: EntityName; - onReady: () => void; -}) => 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) "TechDocsPageWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const TechDocsPageWrapper: ({ children }: Props) => JSX.Element; - -// Warning: (ae-missing-release-tag) "TechDocsPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const TechDocsPicker: () => null; -// Warning: (ae-missing-release-tag) "techdocsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public const techdocsPlugin: BackstagePlugin< { root: RouteRef; @@ -326,12 +285,62 @@ const techdocsPlugin: BackstagePlugin< export { techdocsPlugin as plugin }; export { techdocsPlugin }; -// Warning: (ae-missing-release-tag) "TechDocsReaderPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const TechDocsReaderPage: ({ - children, -}: TechDocsPageProps) => JSX.Element; +// @public +export const TechDocsReaderPage: ( + props: TechDocsReaderPageProps, +) => JSX.Element; + +// @public +export const TechDocsReaderPageHeader: ( + props: TechDocsReaderPageHeaderProps, +) => JSX.Element; + +// @public +export type TechDocsReaderPageHeaderProps = PropsWithChildren<{ + entityRef: EntityName; + entityMetadata?: TechDocsEntityMetadata; + techDocsMetadata?: TechDocsMetadata; +}>; + +// @public +export type TechDocsReaderPageProps = { + children?: TechDocsReaderPageRenderFunction | React_2.ReactNode; +}; + +// @public +export type TechDocsReaderPageRenderFunction = ({ + techdocsMetadataValue, + entityMetadataValue, + entityRef, +}: { + techdocsMetadataValue?: TechDocsMetadata | undefined; + entityMetadataValue?: TechDocsEntityMetadata | undefined; + entityRef: EntityName; + onReady: () => void; +}) => JSX.Element; + +// @public +export const TechDocsSearch: (props: TechDocsSearchProps) => JSX.Element; + +// @public +export type TechDocsSearchProps = { + entityId: EntityName; + debounceTime?: number; +}; + +// @public +export const TechDocsSearchResultListItem: ( + props: TechDocsSearchResultListItemProps, +) => JSX.Element; + +// @public +export type TechDocsSearchResultListItemProps = { + result: any; + lineClamp?: number; + asListItem?: boolean; + asLink?: boolean; + title?: string; +}; // @public export interface TechDocsStorageApi { @@ -390,10 +399,4 @@ export class TechDocsStorageClient implements TechDocsStorageApi { logHandler?: (line: string) => void, ): Promise; } - -// Warnings were encountered during analysis: -// -// src/home/components/EntityListDocsTable.d.ts:11:5 - (ae-forgotten-export) The symbol "columnFactories" needs to be exported by the entry point index.d.ts -// src/home/components/EntityListDocsTable.d.ts:12:5 - (ae-forgotten-export) The symbol "actionFactories" needs to be exported by the entry point index.d.ts -// src/plugin.d.ts:29:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 794e386136..42e57f8be8 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.3", + "version": "0.14.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,6 +11,9 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -22,27 +25,27 @@ "techdocs" ], "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", - "@backstage/config": "^0.1.13", - "@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.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", + "@backstage/catalog-model": "^0.11.0", + "@backstage/config": "^0.1.15", + "@backstage/core-components": "^0.8.10", + "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.7.5", + "@backstage/integration-react": "^0.1.23", + "@backstage/plugin-catalog": "^0.9.0", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-search": "^0.7.1", + "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -53,7 +56,7 @@ "lodash": "^4.17.21", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-text-truncate": "^0.17.0", + "react-text-truncate": "^0.18.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -62,10 +65,10 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.2", - "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21", - "@backstage/test-utils": "^0.2.4", + "@backstage/cli": "^0.14.1", + "@backstage/core-app-api": "^0.5.4", + "@backstage/dev-utils": "^0.2.23", + "@backstage/test-utils": "^0.2.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index 102cda979a..3a49f6edf9 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -19,15 +19,25 @@ import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; import { Route, Routes } from 'react-router-dom'; import { TechDocsIndexPage } from './home/components/TechDocsIndexPage'; -import { TechDocsPage as TechDocsReaderPage } from './reader/components/TechDocsPage'; +import { TechDocsReaderPage } from './reader/components/TechDocsReaderPage'; import { EntityPageDocs } from './EntityPageDocs'; import { MissingAnnotationEmptyState } from '@backstage/core-components'; const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; +/** + * Helper that takes in entity and returns true/false if TechDocs is available for the entity + * + * @public + */ export const isTechDocsAvailable = (entity: Entity) => Boolean(entity?.metadata?.annotations?.[TECHDOCS_ANNOTATION]); +/** + * Responsible for registering routes for TechDocs, TechDocs Homepage and separate TechDocs page + * + * @public + */ export const Router = () => { return ( @@ -40,12 +50,12 @@ export const Router = () => { ); }; -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; - -export const EmbeddedDocsRouter = (_props: Props) => { +/** + * Responsible for registering route to view docs on Entity page + * + * @public + */ +export const EmbeddedDocsRouter = () => { const { entity } = useEntity(); const projectId = entity.metadata.annotations?.[TECHDOCS_ANNOTATION]; diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 8618c4432f..33f5e0a08a 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -23,8 +23,8 @@ import { import { CatalogApi, catalogApiRef, - DefaultStarredEntitiesApi, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { MockStorageApi, @@ -73,7 +73,7 @@ describe('TechDocs Home', () => { [catalogApiRef, mockCatalogApi], [configApiRef, configApi], [storageApiRef, storageApi], - [starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi })], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], ); it('should render a TechDocs home page', async () => { diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx index ff4bf63724..4dcff501ed 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx @@ -34,20 +34,28 @@ import { UserListFilterKind, UserListPicker, } from '@backstage/plugin-catalog-react'; -import { EntityListDocsTable } from './EntityListDocsTable'; import { TechDocsPageWrapper } from './TechDocsPageWrapper'; import { TechDocsPicker } from './TechDocsPicker'; -import { DocsTableRow } from './types'; +import { DocsTableRow, EntityListDocsTable } from './Tables'; -export const DefaultTechDocsHome = ({ - initialFilter = 'all', - columns, - actions, -}: { +/** + * Props for {@link DefaultTechDocsHome} + * + * @public + */ +export type DefaultTechDocsHomeProps = { initialFilter?: UserListFilterKind; columns?: TableColumn[]; actions?: TableProps['actions']; -}) => { +}; + +/** + * Component which renders a default documentation landing page. + * + * @public + */ +export const DefaultTechDocsHome = (props: DefaultTechDocsHomeProps) => { + const { initialFilter = 'all', columns, actions } = props; return ( diff --git a/plugins/techdocs/src/home/components/DocsCardGrid.test.tsx b/plugins/techdocs/src/home/components/Grids/DocsCardGrid.test.tsx similarity index 98% rename from plugins/techdocs/src/home/components/DocsCardGrid.test.tsx rename to plugins/techdocs/src/home/components/Grids/DocsCardGrid.test.tsx index e52636a54a..311f83544a 100644 --- a/plugins/techdocs/src/home/components/DocsCardGrid.test.tsx +++ b/plugins/techdocs/src/home/components/Grids/DocsCardGrid.test.tsx @@ -18,7 +18,7 @@ import { configApiRef } from '@backstage/core-plugin-api'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import React from 'react'; -import { rootDocsRouteRef } from '../../routes'; +import { rootDocsRouteRef } from '../../../routes'; import { DocsCardGrid } from './DocsCardGrid'; // Hacky way to mock a specific boolean config value. diff --git a/plugins/techdocs/src/home/components/DocsCardGrid.tsx b/plugins/techdocs/src/home/components/Grids/DocsCardGrid.tsx similarity index 84% rename from plugins/techdocs/src/home/components/DocsCardGrid.tsx rename to plugins/techdocs/src/home/components/Grids/DocsCardGrid.tsx index 1aef793bd2..6f1d9fbe61 100644 --- a/plugins/techdocs/src/home/components/DocsCardGrid.tsx +++ b/plugins/techdocs/src/home/components/Grids/DocsCardGrid.tsx @@ -14,25 +14,34 @@ * limitations under the License. */ -import React from 'react'; - +import { rootDocsRouteRef } from '../../../routes'; +import { toLowerMaybe } from '../../../helpers'; import { Entity } from '@backstage/catalog-model'; import { useApi, useRouteRef, configApiRef } from '@backstage/core-plugin-api'; -import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core'; -import { rootDocsRouteRef } from '../../routes'; - import { Button, ItemCardGrid, ItemCardHeader, } from '@backstage/core-components'; -import { toLowerMaybe } from '../../helpers'; +import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core'; +import React from 'react'; -export const DocsCardGrid = ({ - entities, -}: { +/** + * Props for {@link DocsCardGrid} + * + * @public + */ +export type DocsCardGridProps = { entities: Entity[] | undefined; -}) => { +}; + +/** + * Component which accepts a list of entities and renders a item card for each entity + * + * @public + */ +export const DocsCardGrid = (props: DocsCardGridProps) => { + const { entities } = props; const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef); const config = useApi(configApiRef); if (!entities) return null; diff --git a/plugins/techdocs/src/home/components/EntityListDocsGrid.tsx b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.tsx similarity index 84% rename from plugins/techdocs/src/home/components/EntityListDocsGrid.tsx rename to plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.tsx index 8c04a29f66..348104f5cc 100644 --- a/plugins/techdocs/src/home/components/EntityListDocsGrid.tsx +++ b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.tsx @@ -13,17 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { DocsCardGrid } from './DocsCardGrid'; import { CodeSnippet, Progress, WarningPanel, } from '@backstage/core-components'; -import { useEntityListProvider } from '@backstage/plugin-catalog-react'; +import { useEntityList } from '@backstage/plugin-catalog-react'; import React from 'react'; -import { DocsCardGrid } from './DocsCardGrid'; +/** + * Component responsible to get entities from entity list context and pass down to DocsCardGrid + * + * @public + */ export const EntityListDocsGrid = () => { - const { loading, error, entities } = useEntityListProvider(); + const { loading, error, entities } = useEntityList(); if (error) { return ( diff --git a/plugins/techdocs/src/home/components/Grids/index.ts b/plugins/techdocs/src/home/components/Grids/index.ts new file mode 100644 index 0000000000..c28b5a0723 --- /dev/null +++ b/plugins/techdocs/src/home/components/Grids/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 './EntityListDocsGrid'; +export * from './DocsCardGrid'; diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx index 34ea416026..536c2c80c8 100644 --- a/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx @@ -17,6 +17,9 @@ import React from 'react'; import { PanelType, TechDocsCustomHome } from './TechDocsCustomHome'; +/** + * @deprecated Use {@link TechDocsCustomHome} instead. + */ export const LegacyTechDocsHome = () => { const tabsConfig = [ { diff --git a/plugins/techdocs/src/home/components/DocsTable.test.tsx b/plugins/techdocs/src/home/components/Tables/DocsTable.test.tsx similarity index 96% rename from plugins/techdocs/src/home/components/DocsTable.test.tsx rename to plugins/techdocs/src/home/components/Tables/DocsTable.test.tsx index 4564725db6..730354f253 100644 --- a/plugins/techdocs/src/home/components/DocsTable.test.tsx +++ b/plugins/techdocs/src/home/components/Tables/DocsTable.test.tsx @@ -18,7 +18,7 @@ import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { configApiRef } from '@backstage/core-plugin-api'; import { DocsTable } from './DocsTable'; -import { rootDocsRouteRef } from '../../routes'; +import { rootDocsRouteRef } from '../../../routes'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; // Hacky way to mock a specific boolean config value. @@ -66,6 +66,7 @@ describe('DocsTable test', () => { namespace: 'default', name: 'owned', }, + targetRef: 'user:default/owned', type: 'ownedBy', }, ], @@ -86,6 +87,7 @@ describe('DocsTable test', () => { namespace: 'default', name: 'not-owned', }, + targetRef: 'user:default/not-owned', type: 'ownedBy', }, ], @@ -137,6 +139,7 @@ describe('DocsTable test', () => { namespace: 'default', name: 'owned', }, + targetRef: 'user:default/owned', type: 'ownedBy', }, ], diff --git a/plugins/techdocs/src/home/components/DocsTable.tsx b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx similarity index 84% rename from plugins/techdocs/src/home/components/DocsTable.tsx rename to plugins/techdocs/src/home/components/Tables/DocsTable.tsx index a00d608b0f..883c496191 100644 --- a/plugins/techdocs/src/home/components/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx @@ -20,10 +20,10 @@ import useCopyToClipboard from 'react-use/lib/useCopyToClipboard'; import { useRouteRef, useApi, configApiRef } from '@backstage/core-plugin-api'; import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { - formatEntityRefTitle, + humanizeEntityRef, getEntityRelations, } from '@backstage/plugin-catalog-react'; -import { rootDocsRouteRef } from '../../routes'; +import { rootDocsRouteRef } from '../../../routes'; import { Button, EmptyState, @@ -31,24 +31,31 @@ import { TableColumn, TableProps, } from '@backstage/core-components'; -import * as actionFactories from './actions'; -import * as columnFactories from './columns'; +import { actionFactories } from './actions'; +import { columnFactories } from './columns'; +import { toLowerMaybe } from '../../../helpers'; import { DocsTableRow } from './types'; -import { toLowerMaybe } from '../../helpers'; -export const DocsTable = ({ - entities, - title, - loading, - columns, - actions, -}: { +/** + * Props for {@link DocsTable}. + * + * @public + */ +export type DocsTableProps = { entities: Entity[] | undefined; title?: string | undefined; loading?: boolean | undefined; columns?: TableColumn[]; actions?: TableProps['actions']; -}) => { +}; + +/** + * Component which renders a table documents + * + * @public + */ +export const DocsTable = (props: DocsTableProps) => { + const { entities, title, loading, columns, actions } = props; const [, copyToClipboard] = useCopyToClipboard(); const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef); const config = useApi(configApiRef); @@ -69,7 +76,7 @@ export const DocsTable = ({ }), ownedByRelations, ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .map(r => humanizeEntityRef(r, { defaultKind: 'group' })) .join(', '), }, }; @@ -124,3 +131,6 @@ export const DocsTable = ({ ); }; + +DocsTable.columns = columnFactories; +DocsTable.actions = actionFactories; diff --git a/plugins/techdocs/src/home/components/EntityListDocsTable.tsx b/plugins/techdocs/src/home/components/Tables/EntityListDocsTable.tsx similarity index 80% rename from plugins/techdocs/src/home/components/EntityListDocsTable.tsx rename to plugins/techdocs/src/home/components/Tables/EntityListDocsTable.tsx index 4d136591ff..d47ddb13c4 100644 --- a/plugins/techdocs/src/home/components/EntityListDocsTable.tsx +++ b/plugins/techdocs/src/home/components/Tables/EntityListDocsTable.tsx @@ -24,22 +24,32 @@ import { WarningPanel, } from '@backstage/core-components'; import { - useEntityListProvider, + useEntityList, useStarredEntities, } from '@backstage/plugin-catalog-react'; import { DocsTable } from './DocsTable'; -import * as actionFactories from './actions'; -import * as columnFactories from './columns'; +import { actionFactories } from './actions'; +import { columnFactories } from './columns'; import { DocsTableRow } from './types'; -export const EntityListDocsTable = ({ - columns, - actions, -}: { +/** + * Props for {@link EntityListDocsTable}. + * + * @public + */ +export type EntityListDocsTableProps = { columns?: TableColumn[]; actions?: TableProps['actions']; -}) => { - const { loading, error, entities, filters } = useEntityListProvider(); +}; + +/** + * Component which renders a table with entities from catalog. + * + * @public + */ +export const EntityListDocsTable = (props: EntityListDocsTableProps) => { + const { columns, actions } = props; + const { loading, error, entities, filters } = useEntityList(); const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); const [, copyToClipboard] = useCopyToClipboard(); diff --git a/plugins/techdocs/src/home/components/Tables/actions.tsx b/plugins/techdocs/src/home/components/Tables/actions.tsx new file mode 100644 index 0000000000..d0dbb8bf47 --- /dev/null +++ b/plugins/techdocs/src/home/components/Tables/actions.tsx @@ -0,0 +1,55 @@ +/* + * 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 ShareIcon from '@material-ui/icons/Share'; +import { + favoriteEntityIcon, + favoriteEntityTooltip, +} from '@backstage/plugin-catalog-react'; +import { DocsTableRow } from './types'; + +/** + * Not directly exported, but through DocsTable.actions and EntityListDocsTable.actions + * + * @public + */ +export const actionFactories = { + createCopyDocsUrlAction(copyToClipboard: Function) { + return (row: DocsTableRow) => { + return { + icon: () => , + tooltip: 'Click to copy documentation link to clipboard', + onClick: () => + copyToClipboard(`${window.location.origin}${row.resolved.docsUrl}`), + }; + }; + }, + createStarEntityAction( + isStarredEntity: Function, + toggleStarredEntity: Function, + ) { + return ({ entity }: DocsTableRow) => { + const isStarred = isStarredEntity(entity); + return { + cellStyle: { paddingLeft: '1em' }, + icon: () => favoriteEntityIcon(isStarred), + tooltip: favoriteEntityTooltip(isStarred), + onClick: () => toggleStarredEntity(entity), + }; + }; + }, +}; diff --git a/plugins/techdocs/src/home/components/Tables/columns.tsx b/plugins/techdocs/src/home/components/Tables/columns.tsx new file mode 100644 index 0000000000..774cfc5e8a --- /dev/null +++ b/plugins/techdocs/src/home/components/Tables/columns.tsx @@ -0,0 +1,66 @@ +/* + * 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 { Link, SubvalueCell, TableColumn } from '@backstage/core-components'; +import { EntityRefLinks } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; +import { DocsTableRow } from './types'; + +function customTitle(entity: Entity): string { + return entity.metadata.title || entity.metadata.name; +} + +/** + * Not directly exported, but through DocsTable.columns and EntityListDocsTable.columns + * + * @public + */ +export const columnFactories = { + createNameColumn(): TableColumn { + return { + title: 'Document', + field: 'entity.metadata.name', + highlight: true, + render: (row: DocsTableRow) => ( + {customTitle(row.entity)} + } + subvalue={row.entity.metadata.description} + /> + ), + }; + }, + createOwnerColumn(): TableColumn { + return { + title: 'Owner', + field: 'resolved.ownedByRelationsTitle', + render: ({ resolved }) => ( + + ), + }; + }, + createTypeColumn(): TableColumn { + return { + title: 'Type', + field: 'entity.spec.type', + }; + }, +}; diff --git a/plugins/techdocs/src/home/components/Tables/index.ts b/plugins/techdocs/src/home/components/Tables/index.ts new file mode 100644 index 0000000000..2db7f0ae89 --- /dev/null +++ b/plugins/techdocs/src/home/components/Tables/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './EntityListDocsTable'; +export * from './DocsTable'; +export * from './types'; diff --git a/plugins/techdocs/src/home/components/types.ts b/plugins/techdocs/src/home/components/Tables/types.ts similarity index 90% rename from plugins/techdocs/src/home/components/types.ts rename to plugins/techdocs/src/home/components/Tables/types.ts index 6fd29f1716..c076d4c12d 100644 --- a/plugins/techdocs/src/home/components/types.ts +++ b/plugins/techdocs/src/home/components/Tables/types.ts @@ -16,6 +16,11 @@ import { Entity, EntityName } from '@backstage/catalog-model'; +/** + * Generic representing the metadata structure for a docs table row. + * + * @public + */ export type DocsTableRow = { entity: Entity; resolved: { diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx index b32cadaa05..c8149f1ff2 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx @@ -15,7 +15,7 @@ */ import React, { useState } from 'react'; -import useAsync from 'react-use/lib/useAsync'; +import useAsync, { AsyncState } from 'react-use/lib/useAsync'; import { makeStyles } from '@material-ui/core'; import { CSSProperties } from '@material-ui/styles'; import { @@ -23,11 +23,15 @@ import { catalogApiRef, CatalogApi, isOwnerOf, - useOwnUser, } from '@backstage/plugin-catalog-react'; -import { Entity } from '@backstage/catalog-model'; -import { DocsTable } from './DocsTable'; -import { DocsCardGrid } from './DocsCardGrid'; +import { + DEFAULT_NAMESPACE, + Entity, + parseEntityRef, + UserEntity, +} from '@backstage/catalog-model'; +import { DocsTable } from './Tables'; +import { DocsCardGrid } from './Grids'; import { TechDocsPageWrapper } from './TechDocsPageWrapper'; import { @@ -40,15 +44,25 @@ import { ContentHeader, } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; const panels = { DocsTable: DocsTable, DocsCardGrid: DocsCardGrid, }; +/** + * Available panel types + * + * @public + */ export type PanelType = 'DocsCardGrid' | 'DocsTable'; +/** + * Type representing a TechDocsCustomHome panel. + * + * @public + */ export interface PanelConfig { title: string; description: string; @@ -57,11 +71,21 @@ export interface PanelConfig { filterPredicate: ((entity: Entity) => boolean) | string; } +/** + * Type representing a TechDocsCustomHome tab. + * + * @public + */ export interface TabConfig { label: string; panels: PanelConfig[]; } +/** + * Type representing a list of TechDocsCustomHome tabs. + * + * @public + */ export type TabsConfig = TabConfig[]; const CustomPanel = ({ @@ -114,11 +138,17 @@ const CustomPanel = ({ ); }; -export const TechDocsCustomHome = ({ - tabsConfig, -}: { +/** + * Props for {@link TechDocsCustomHome} + * + * @public + */ +export type TechDocsCustomHomeProps = { tabsConfig: TabsConfig; -}) => { +}; + +export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => { + const { tabsConfig } = props; const [selectedTab, setSelectedTab] = useState(0); const catalogApi: CatalogApi = useApi(catalogApiRef); @@ -195,3 +225,18 @@ export const TechDocsCustomHome = ({ ); }; + +function useOwnUser(): AsyncState { + const catalogApi = useApi(catalogApiRef); + const identityApi = useApi(identityApiRef); + + return useAsync(async () => { + const identity = await identityApi.getBackstageIdentity(); + return catalogApi.getEntityByName( + parseEntityRef(identity.userEntityRef, { + defaultKind: 'User', + defaultNamespace: DEFAULT_NAMESPACE, + }), + ) as Promise; + }, [catalogApi, identityApi]); +} diff --git a/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx b/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx index fbeca7cfd4..1d018eec70 100644 --- a/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx +++ b/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx @@ -24,8 +24,8 @@ jest.mock('react-router', () => ({ useOutlet: jest.fn().mockReturnValue('Route Children'), })); -jest.mock('./LegacyTechDocsHome', () => ({ - LegacyTechDocsHome: jest.fn().mockReturnValue('LegacyTechDocsHomeMock'), +jest.mock('./DefaultTechDocsHome', () => ({ + DefaultTechDocsHome: jest.fn().mockReturnValue('DefaultTechDocsHomeMock'), })); describe('TechDocsIndexPage', () => { @@ -35,10 +35,10 @@ describe('TechDocsIndexPage', () => { expect(getByText('Route Children')).toBeInTheDocument(); }); - it('renders legacy TechDocs home when no router children are provided', async () => { + it('renders DefaultTechDocsHome when no router children are provided', async () => { (useOutlet as jest.Mock).mockReturnValueOnce(null); const { getByText } = await renderInTestApp(); - expect(getByText('LegacyTechDocsHomeMock')).toBeInTheDocument(); + expect(getByText('DefaultTechDocsHomeMock')).toBeInTheDocument(); }); }); diff --git a/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx b/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx index ff694d3570..4467baf2a1 100644 --- a/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx +++ b/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx @@ -16,10 +16,10 @@ import React from 'react'; import { useOutlet } from 'react-router'; -import { LegacyTechDocsHome } from './LegacyTechDocsHome'; +import { DefaultTechDocsHome } from './DefaultTechDocsHome'; export const TechDocsIndexPage = () => { const outlet = useOutlet(); - return outlet || ; + return outlet || ; }; diff --git a/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx b/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx index 453cfc22cc..52511bda19 100644 --- a/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx +++ b/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx @@ -19,11 +19,22 @@ import React from 'react'; import { PageWithHeader } from '@backstage/core-components'; import { useApi, configApiRef } from '@backstage/core-plugin-api'; -type Props = { +/** + * Props for {@link TechDocsPageWrapper} + * + * @public + */ +export type TechDocsPageWrapperProps = { children?: React.ReactNode; }; -export const TechDocsPageWrapper = ({ children }: Props) => { +/** + * Component wrapping a techdocs page with Page and Header components + * + * @public + */ +export const TechDocsPageWrapper = (props: TechDocsPageWrapperProps) => { + const { children } = props; const configApi = useApi(configApiRef); const generatedSubtitle = `Documentation available in ${ configApi.getOptionalString('organization.name') ?? 'Backstage' diff --git a/plugins/techdocs/src/home/components/TechDocsPicker.tsx b/plugins/techdocs/src/home/components/TechDocsPicker.tsx index 9ed21da368..32c7b800eb 100644 --- a/plugins/techdocs/src/home/components/TechDocsPicker.tsx +++ b/plugins/techdocs/src/home/components/TechDocsPicker.tsx @@ -19,7 +19,7 @@ import { CATALOG_FILTER_EXISTS, DefaultEntityFilters, EntityFilter, - useEntityListProvider, + useEntityList, } from '@backstage/plugin-catalog-react'; class TechDocsFilter implements EntityFilter { @@ -34,8 +34,13 @@ type CustomFilters = DefaultEntityFilters & { techdocs?: TechDocsFilter; }; +/** + * Component responsible for updating TechDocs filters + * + * @public + */ export const TechDocsPicker = () => { - const { updateFilters } = useEntityListProvider(); + const { updateFilters } = useEntityList(); useEffect(() => { updateFilters({ diff --git a/plugins/techdocs/src/home/components/actions.tsx b/plugins/techdocs/src/home/components/actions.tsx deleted file mode 100644 index eb09c1d429..0000000000 --- a/plugins/techdocs/src/home/components/actions.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 ShareIcon from '@material-ui/icons/Share'; -import { - favoriteEntityIcon, - favoriteEntityTooltip, -} from '@backstage/plugin-catalog-react'; -import { DocsTableRow } from './types'; - -export function createCopyDocsUrlAction(copyToClipboard: Function) { - return (row: DocsTableRow) => { - return { - icon: () => , - tooltip: 'Click to copy documentation link to clipboard', - onClick: () => - copyToClipboard(`${window.location.origin}${row.resolved.docsUrl}`), - }; - }; -} - -export function createStarEntityAction( - isStarredEntity: Function, - toggleStarredEntity: Function, -) { - return ({ entity }: DocsTableRow) => { - const isStarred = isStarredEntity(entity); - return { - cellStyle: { paddingLeft: '1em' }, - icon: () => favoriteEntityIcon(isStarred), - tooltip: favoriteEntityTooltip(isStarred), - onClick: () => toggleStarredEntity(entity), - }; - }; -} diff --git a/plugins/techdocs/src/home/components/columns.tsx b/plugins/techdocs/src/home/components/columns.tsx deleted file mode 100644 index d29f44da75..0000000000 --- a/plugins/techdocs/src/home/components/columns.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 { Link, SubvalueCell, TableColumn } from '@backstage/core-components'; -import { EntityRefLinks } from '@backstage/plugin-catalog-react'; -import { Entity } from '@backstage/catalog-model'; -import { DocsTableRow } from './types'; - -function customTitle(entity: Entity): string { - return entity.metadata.title || entity.metadata.name; -} - -export function createNameColumn(): TableColumn { - return { - title: 'Document', - field: 'entity.metadata.name', - highlight: true, - render: (row: DocsTableRow) => ( - {customTitle(row.entity)}} - subvalue={row.entity.metadata.description} - /> - ), - }; -} - -export function createOwnerColumn(): TableColumn { - return { - title: 'Owner', - field: 'resolved.ownedByRelationsTitle', - render: ({ resolved }) => ( - - ), - }; -} - -export function createTypeColumn(): TableColumn { - return { - title: 'Type', - field: 'entity.spec.type', - }; -} diff --git a/plugins/techdocs/src/home/components/index.ts b/plugins/techdocs/src/home/components/index.ts index d9d627cf8d..d21f992cef 100644 --- a/plugins/techdocs/src/home/components/index.ts +++ b/plugins/techdocs/src/home/components/index.ts @@ -14,10 +14,15 @@ * limitations under the License. */ -export { DefaultTechDocsHome } from './DefaultTechDocsHome'; -export { EntityListDocsGrid } from './EntityListDocsGrid'; -export { EntityListDocsTable } from './EntityListDocsTable'; -export type { PanelType } from './TechDocsCustomHome'; -export { TechDocsPageWrapper } from './TechDocsPageWrapper'; -export { TechDocsPicker } from './TechDocsPicker'; -export type { DocsTableRow } from './types'; +export * from './Grids'; +export * from './Tables'; +export * from './DefaultTechDocsHome'; +export type { + PanelType, + PanelConfig, + TabConfig, + TabsConfig, + TechDocsCustomHomeProps, +} from './TechDocsCustomHome'; +export * from './TechDocsPageWrapper'; +export * from './TechDocsPicker'; diff --git a/plugins/techdocs/src/home/index.ts b/plugins/techdocs/src/home/index.ts new file mode 100644 index 0000000000..1be7f65c25 --- /dev/null +++ b/plugins/techdocs/src/home/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './components'; diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index a6a95e9264..c31f8b7370 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -20,29 +20,19 @@ * @packageDocumentation */ +export * from './types'; export * from './api'; -export { techdocsApiRef, techdocsStorageApiRef } from './api'; -export type { TechDocsApi, TechDocsStorageApi } from './api'; -export { TechDocsClient, TechDocsStorageClient } from './client'; -export * from './components/DocsResultListItem'; +export * from './client'; +export * from './reader'; +export * from './search'; +export * from './home'; export { - DefaultTechDocsHome, - EntityListDocsGrid, - EntityListDocsTable, - TechDocsPageWrapper, - TechDocsPicker, -} from './home/components'; -export type { DocsTableRow, PanelType } from './home/components'; -export { - DocsCardGrid, - DocsTable, EntityTechdocsContent, TechDocsCustomHome, TechDocsIndexPage, TechdocsPage, + TechDocsReaderPage, techdocsPlugin as plugin, techdocsPlugin, - TechDocsReaderPage, } from './plugin'; -export * from './reader'; -export { EmbeddedDocsRouter, isTechDocsAvailable, Router } from './Router'; +export * from './Router'; diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index a37ad61f80..2e3967cb6d 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -24,7 +24,6 @@ import { import { configApiRef, createApiFactory, - createComponentExtension, createPlugin, createRoutableExtension, discoveryApiRef, @@ -32,6 +31,11 @@ import { identityApiRef, } from '@backstage/core-plugin-api'; +/** + * The Backstage plugin that renders technical documentation for your components + * + * @public + */ export const techdocsPlugin = createPlugin({ id: 'techdocs', apis: [ @@ -73,6 +77,11 @@ export const techdocsPlugin = createPlugin({ }, }); +/** + * Routable extension used to render docs + * + * @public + */ export const TechdocsPage = techdocsPlugin.provide( createRoutableExtension({ name: 'TechdocsPage', @@ -81,6 +90,11 @@ export const TechdocsPage = techdocsPlugin.provide( }), ); +/** + * Routable extension used to render docs on Entity page + * + * @public + */ export const EntityTechdocsContent = techdocsPlugin.provide( createRoutableExtension({ name: 'EntityTechdocsContent', @@ -89,28 +103,11 @@ export const EntityTechdocsContent = techdocsPlugin.provide( }), ); -// takes a list of entities and renders documentation cards -export const DocsCardGrid = techdocsPlugin.provide( - createComponentExtension({ - name: 'DocsCardGrid', - component: { - lazy: () => - import('./home/components/DocsCardGrid').then(m => m.DocsCardGrid), - }, - }), -); - -// takes a list of entities and renders table listing documentation -export const DocsTable = techdocsPlugin.provide( - createComponentExtension({ - name: 'DocsTable', - component: { - lazy: () => import('./home/components/DocsTable').then(m => m.DocsTable), - }, - }), -); - -// takes a custom tabs config object and renders a documentation landing page +/** + * Component which takes a custom tabs config object and renders a documentation landing page. + * + * @public + */ export const TechDocsCustomHome = techdocsPlugin.provide( createRoutableExtension({ name: 'TechDocsCustomHome', @@ -122,6 +119,11 @@ export const TechDocsCustomHome = techdocsPlugin.provide( }), ); +/** + * Responsible for rendering the provided router element + * + * @public + */ export const TechDocsIndexPage = techdocsPlugin.provide( createRoutableExtension({ name: 'TechDocsIndexPage', @@ -133,11 +135,18 @@ export const TechDocsIndexPage = techdocsPlugin.provide( }), ); +/** + * Component responsible for composing a TechDocs reader page experience + * + * @public + */ export const TechDocsReaderPage = techdocsPlugin.provide( createRoutableExtension({ name: 'TechDocsReaderPage', component: () => - import('./reader/components/TechDocsPage').then(m => m.TechDocsPage), + import('./reader/components/TechDocsReaderPage').then( + m => m.TechDocsReaderPage, + ), mountPoint: rootDocsRouteRef, }), ); diff --git a/plugins/techdocs/src/reader/components/LegacyTechDocsPage.tsx b/plugins/techdocs/src/reader/components/LegacyTechDocsPage.tsx index f3384e21b3..4bfda57ace 100644 --- a/plugins/techdocs/src/reader/components/LegacyTechDocsPage.tsx +++ b/plugins/techdocs/src/reader/components/LegacyTechDocsPage.tsx @@ -22,8 +22,11 @@ import { TechDocsNotFound } from './TechDocsNotFound'; import { useApi } from '@backstage/core-plugin-api'; import { Page, Content } from '@backstage/core-components'; import { Reader } from './Reader'; -import { TechDocsPageHeader } from './TechDocsPageHeader'; +import { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader'; +/** + * @deprecated Use {@link TechDocsReaderPage} instead. + */ export const LegacyTechDocsPage = () => { const [documentReady, setDocumentReady] = useState(false); const { namespace, kind, name } = useParams(); @@ -53,7 +56,7 @@ export const LegacyTechDocsPage = () => { return ( - void; @@ -62,12 +75,13 @@ type Props = { const useStyles = makeStyles(theme => ({ searchBar: { - marginLeft: '20rem', - maxWidth: 'calc(100% - 20rem * 2 - 3rem)', - marginTop: theme.spacing(1), + maxWidth: 'calc(100% - 16rem * 2 - 2.4rem)', + marginTop: 0, + marginBottom: theme.spacing(1), + marginLeft: 'calc(16rem + 1.2rem)', '@media screen and (max-width: 76.1875em)': { - marginLeft: '10rem', - maxWidth: 'calc(100% - 10rem)', + marginLeft: '0', + maxWidth: '100%', }, }, })); @@ -121,6 +135,15 @@ export const withTechDocsReaderProvider = */ export const useTechDocsReader = () => useContext(TechDocsReaderContext); +type TypographyHeadings = Pick< + Theme['typography'], + 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' +>; + +type TypographyHeadingsKeys = keyof TypographyHeadings; + +const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + /** * Hook that encapsulates the behavior of getting raw HTML and applying * transforms to it in order to make it function at a basic level in the @@ -154,11 +177,20 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { if (!dom || !sidebars) return; // set sidebar height so they don't initially render in wrong position const mdTabs = dom.querySelector('.md-container > .md-tabs'); + const sidebarsCollapsed = window.matchMedia( + 'screen and (max-width: 76.1875em)', + ).matches; + const newTop = Math.max(dom.getBoundingClientRect().top, 0); sidebars.forEach(sidebar => { - const newTop = Math.max(dom.getBoundingClientRect().top, 0); - sidebar.style.top = mdTabs - ? `${newTop + mdTabs.getBoundingClientRect().height}px` - : `${newTop}px`; + if (sidebarsCollapsed) { + sidebar.style.top = '0px'; + } else if (mdTabs) { + sidebar.style.top = `${ + newTop + mdTabs.getBoundingClientRect().height + }px`; + } else { + sidebar.style.top = `${newTop}px`; + } }); }, [dom, sidebars]); @@ -205,189 +237,512 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { path: contentPath, }), rewriteDocLinks(), + addSidebarToggle(), removeMkdocsHeader(), simplifyMkdocsFooter(), addGitFeedbackLink(scmIntegrationsApi), injectCss({ + // Variables css: ` - body { - font-family: ${theme.typography.fontFamily}; - --md-text-color: ${theme.palette.text.primary}; - --md-text-link-color: ${theme.palette.primary.main}; + /* + As the MkDocs output is rendered in shadow DOM, the CSS variable definitions on the root selector are not applied. Instead, they have to be applied on :host. + As there is no way to transform the served main*.css yet (for example in the backend), we have to copy from main*.css and modify them. + */ + :host { + /* FONT */ + --md-default-fg-color: ${theme.palette.text.primary}; + --md-default-fg-color--light: ${theme.palette.text.secondary}; + --md-default-fg-color--lighter: ${lighten( + theme.palette.text.secondary, + 0.7, + )}; + --md-default-fg-color--lightest: ${lighten( + theme.palette.text.secondary, + 0.3, + )}; - --md-code-fg-color: ${theme.palette.text.primary}; - --md-code-bg-color: ${theme.palette.background.paper}; - --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; } - .md-content { margin-bottom: 50px } - .md-footer { position: fixed; bottom: 0px; } - .md-footer-nav__link { width: 20rem;} - .md-content { margin-left: 20rem; max-width: calc(100% - 20rem * 2 - 3rem); } - .md-typeset { font-size: 1rem; } - .md-typeset h1, .md-typeset h2, .md-typeset h3 { font-weight: bold; } - .md-nav { font-size: 1rem; } - .md-grid { max-width: 90vw; margin: 0 } - .md-typeset blockquote { - color: ${theme.palette.textSubtle}; - border-left: 0.2rem solid ${theme.palette.textVerySubtle}; - } - .md-typeset hr { - border-bottom: 0.05rem dotted ${theme.palette.textVerySubtle}; - } - .md-typeset table:not([class]) { - font-size: 1rem; - border: 1px solid ${theme.palette.text.primary}; - border-bottom: none; - border-collapse: collapse; - } - .md-typeset table:not([class]) td, .md-typeset table:not([class]) th { - border-bottom: 1px solid ${theme.palette.text.primary}; - } - .md-typeset table:not([class]) th { font-weight: bold; } - .md-typeset .admonition, .md-typeset details { - font-size: 1rem; + /* BACKGROUND */ + --md-default-bg-color:${theme.palette.background.default}; + --md-default-bg-color--light: ${theme.palette.background.paper}; + --md-default-bg-color--lighter: ${lighten( + theme.palette.background.paper, + 0.7, + )}; + --md-default-bg-color--lightest: ${lighten( + theme.palette.background.paper, + 0.3, + )}; + + /* PRIMARY */ + --md-primary-fg-color: ${theme.palette.primary.main}; + --md-primary-fg-color--light: ${theme.palette.primary.light}; + --md-primary-fg-color--dark: ${theme.palette.primary.dark}; + --md-primary-bg-color: ${theme.palette.primary.contrastText}; + --md-primary-bg-color--light: ${lighten( + theme.palette.primary.contrastText, + 0.7, + )}; + + /* ACCENT */ + --md-accent-fg-color: var(--md-primary-fg-color); + + /* SHADOW */ + --md-shadow-z1: ${theme.shadows[1]}; + --md-shadow-z2: ${theme.shadows[2]}; + --md-shadow-z3: ${theme.shadows[3]}; + + /* EXTENSIONS */ + --md-admonition-fg-color: var(--md-default-fg-color); + --md-admonition-bg-color: var(--md-default-bg-color); + /* Admonitions and others are using SVG masks to define icons. These masks are defined as CSS variables. */ + --md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--info: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--tip: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--success: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--question: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--warning: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--failure: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--danger: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--example: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--quote: url('data:image/svg+xml;charset=utf-8,'); + --md-footnotes-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-details-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-tasklist-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,'); + --md-nav-icon--prev: url('data:image/svg+xml;charset=utf-8,'); + --md-nav-icon--next: url('data:image/svg+xml;charset=utf-8,'); + --md-toc-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-clipboard-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-search-result-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-source-forks-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-source-repositories-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-source-stars-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-source-version-icon: url('data:image/svg+xml;charset=utf-8,'); + --md-version-icon: url('data:image/svg+xml;charset=utf-8,'); } - /* style the checkmarks of the task list */ - .md-typeset .task-list-control .task-list-indicator::before { - background-color: ${theme.palette.action.disabledBackground}; + :host > * { + /* CODE */ + --md-code-fg-color: ${theme.palette.text.primary}; + --md-code-bg-color: ${theme.palette.background.paper}; + --md-code-hl-color: ${alpha(theme.palette.warning.main, 0.5)}; + --md-code-hl-keyword-color: ${ + isDarkTheme + ? theme.palette.primary.light + : theme.palette.primary.dark + }; + --md-code-hl-function-color: ${ + isDarkTheme + ? theme.palette.secondary.light + : theme.palette.secondary.dark + }; + --md-code-hl-string-color: ${ + isDarkTheme + ? theme.palette.success.light + : theme.palette.success.dark + }; + --md-code-hl-number-color: ${ + isDarkTheme ? theme.palette.error.light : theme.palette.error.dark + }; + --md-code-hl-constant-color: var(--md-code-hl-function-color); + --md-code-hl-special-color: var(--md-code-hl-function-color); + --md-code-hl-name-color: var(--md-code-fg-color); + --md-code-hl-comment-color: var(--md-default-fg-color--light); + --md-code-hl-generic-color: var(--md-default-fg-color--light); + --md-code-hl-variable-color: var(--md-default-fg-color--light); + --md-code-hl-operator-color: var(--md-default-fg-color--light); + --md-code-hl-punctuation-color: var(--md-default-fg-color--light); + + /* TYPESET */ + --md-typeset-font-size: 1rem; + --md-typeset-color: var(--md-default-fg-color); + --md-typeset-a-color: var(--md-accent-fg-color); + --md-typeset-table-color: ${theme.palette.text.primary}; + --md-typeset-del-color: ${ + isDarkTheme + ? alpha(theme.palette.error.dark, 0.5) + : alpha(theme.palette.error.light, 0.5) + }; + --md-typeset-ins-color: ${ + isDarkTheme + ? alpha(theme.palette.success.dark, 0.5) + : alpha(theme.palette.success.light, 0.5) + }; + --md-typeset-mark-color: ${ + isDarkTheme + ? alpha(theme.palette.warning.dark, 0.5) + : alpha(theme.palette.warning.light, 0.5) + }; } - .md-typeset .task-list-control [type="checkbox"]:checked + .task-list-indicator:before { - background-color: ${theme.palette.success.main}; - } - /**/ @media screen and (max-width: 76.1875em) { - .md-nav { - background-color: ${theme.palette.background.default}; - transition: none !important - } - .md-sidebar--secondary { display: none; } - .md-sidebar--primary { left: ${ - isPinned ? '242px' : '72px' - }; width: 10rem } - .md-content { margin-left: 10rem; max-width: calc(100% - 10rem); } - .md-content__inner { font-size: 0.9rem } - .md-footer { - position: static; - padding-left: 10rem; - } - .md-footer-nav__link { - /* footer links begin to overlap at small sizes without setting width */ - width: 50%; - } - .md-nav--primary .md-nav__title { - white-space: normal; - height: auto; - line-height: 1rem; - cursor: auto; - } - .md-nav--primary > .md-nav__title [for="none"] { - padding-top: 0; + :host > * { + /* TYPESET */ + --md-typeset-font-size: .9rem; } } - `, + + @media screen and (max-width: 600px) { + :host > * { + /* TYPESET */ + --md-typeset-font-size: .7rem; + } + } + `, }), injectCss({ - // Disable CSS animations on link colors as they lead to issues in dark - // mode. The dark mode color theme is applied later and theirfore there - // is always an animation from light to dark mode when navigation - // between pages. + // Reset css: ` - .md-nav__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink { - transition: none; + body { + --md-text-color: var(--md-default-fg-color); + --md-text-link-color: var(--md-accent-fg-color); + --md-text-font-family: ${theme.typography.fontFamily}; + font-family: var(--md-text-font-family); + background-color: unset; } `, }), injectCss({ - // Properly style code blocks. + // Layout css: ` + .md-grid { + max-width: 100%; + margin: 0; + } + + .md-nav { + font-size: calc(var(--md-typeset-font-size) * 0.9); + } + .md-nav__link { + display: flex; + align-items: center; + justify-content: space-between; + } + .md-nav__icon { + height: 20px !important; + width: 20px !important; + margin-left:${theme.spacing(1)}px; + } + .md-nav__icon svg { + margin: 0; + width: 20px !important; + height: 20px !important; + } + .md-nav__icon:after { + width: 20px !important; + height: 20px !important; + } + + .md-main__inner { + margin-top: 0; + } + + .md-sidebar { + height: calc(100% - 100px); + position: fixed; + width: 16rem; + } + .md-sidebar .md-sidebar__scrollwrap { + max-height: calc(100% - 100px); + } + .md-sidebar--secondary { + right: ${theme.spacing(3)}px; + } + + .md-content { + max-width: calc(100% - 16rem * 2); + margin-left: 16rem; + margin-bottom: 50px; + } + + .md-footer { + position: fixed; + bottom: 0px; + } + .md-footer__title { + background-color: unset; + } + .md-footer-nav__link { + width: 16rem; + } + + .md-dialog { + background-color: unset; + } + + @media screen and (max-width: 76.1875em) { + .md-nav { + transition: none !important; + background-color: var(--md-default-bg-color) + } + .md-nav--primary .md-nav__title { + cursor: auto; + color: var(--md-default-fg-color); + font-weight: 700; + white-space: normal; + line-height: 1rem; + height: auto; + display: flex; + flex-flow: column; + row-gap: 1.6rem; + padding: 1.2rem .8rem .8rem; + background-color: var(--md-default-bg-color); + } + .md-nav--primary .md-nav__title~.md-nav__list { + box-shadow: none; + } + .md-nav--primary .md-nav__title ~ .md-nav__list > :first-child { + border-top: none; + } + .md-nav--primary .md-nav__title .md-nav__button { + display: none; + } + .md-nav--primary .md-nav__title .md-nav__icon { + color: var(--md-default-fg-color); + position: static; + height: auto; + margin: 0 0 0 -0.2rem; + } + .md-nav--primary > .md-nav__title [for="none"] { + padding-top: 0; + } + .md-nav--primary .md-nav__item { + border-top: none; + } + .md-nav--primary :is(.md-nav__title,.md-nav__item) { + font-size : var(--md-typeset-font-size); + } + .md-nav .md-source { + display: none; + } + + .md-sidebar { + height: 100%; + } + .md-sidebar--primary { + width: 12.1rem !important; + z-index: 200; + left: ${ + isPinned ? 'calc(-12.1rem + 242px)' : 'calc(-12.1rem + 72px)' + } !important; + } + .md-sidebar--secondary:not([hidden]) { + display: none; + } + + .md-content { + max-width: 100%; + margin-left: 0; + } + + .md-header__button { + margin: 0.4rem 0; + margin-left: 0.4rem; + padding: 0; + } + + .md-overlay { + left: 0; + } + + .md-footer { + position: static; + padding-left: 0; + } + .md-footer-nav__link { + /* footer links begin to overlap at small sizes without setting width */ + width: 50%; + } + } + + @media screen and (max-width: 600px) { + .md-sidebar--primary { + left: -12.1rem !important; + width: 12.1rem; + } + } + `, + }), + injectCss({ + // Typeset + css: ` + .md-typeset { + font-size: var(--md-typeset-font-size); + } + + ${headings.reduce((style, heading) => { + const styles = theme.typography[heading]; + const { lineHeight, fontFamily, fontWeight, fontSize } = styles; + const calculate = (value: typeof fontSize) => { + let factor: number | string = 1; + if (typeof value === 'number') { + // 60% of the size defined because it is too big + factor = (value / 16) * 0.6; + } + if (typeof value === 'string') { + factor = value.replace('rem', ''); + } + return `calc(${factor} * var(--md-typeset-font-size))`; + }; + return style.concat(` + .md-typeset ${heading} { + color: var(--md-default-fg-color); + line-height: ${lineHeight}; + font-family: ${fontFamily}; + font-weight: ${fontWeight}; + font-size: ${calculate(fontSize)}; + } + `); + }, '')} + + .md-typeset .md-content__button { + color: var(--md-default-fg-color); + } + + .md-typeset hr { + border-bottom: 0.05rem dotted ${theme.palette.divider}; + } + + .md-typeset details { + font-size: var(--md-typeset-font-size) !important; + } + .md-typeset details summary { + padding-left: 2.5rem !important; + } + .md-typeset details summary:before, + .md-typeset details summary:after { + top: 50% !important; + width: 20px !important; + height: 20px !important; + transform: rotate(0deg) translateY(-50%) !important; + } + .md-typeset details[open] > summary:after { + transform: rotate(90deg) translateX(-50%) !important; + } + + .md-typeset blockquote { + color: var(--md-default-fg-color--light); + border-left: 0.2rem solid var(--md-default-fg-color--light); + } + + .md-typeset table:not([class]) { + font-size: var(--md-typeset-font-size); + border: 1px solid var(--md-default-fg-color); + border-bottom: none; + border-collapse: collapse; + } + .md-typeset table:not([class]) th { + font-weight: bold; + } + .md-typeset table:not([class]) td, .md-typeset table:not([class]) th { + border-bottom: 1px solid var(--md-default-fg-color); + } + .md-typeset pre > code::-webkit-scrollbar-thumb { background-color: hsla(0, 0%, 0%, 0.32); } .md-typeset pre > code::-webkit-scrollbar-thumb:hover { background-color: hsla(0, 0%, 0%, 0.87); } - `, + `, }), injectCss({ - // Admonitions and others are using SVG masks to define icons. These - // masks are defined as CSS variables. - // As the MkDocs output is rendered in shadow DOM, the CSS variable - // definitions on the root selector are not applied. Instead, the have - // to be applied on :host. - // As there is no way to transform the served main*.css yet (for - // example in the backend), we have to copy from main*.css and modify - // them. + // Animations css: ` - :host { - --md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--info: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--tip: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--success: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--question: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--warning: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--failure: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--danger: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--example: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-icon--quote: url('data:image/svg+xml;charset=utf-8,'); + /* + Disable CSS animations on link colors as they lead to issues in dark mode. + The dark mode color theme is applied later and theirfore there is always an animation from light to dark mode when navigation between pages. + */ + .md-dialog, .md-nav__link, .md-footer__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink { + transition: none; } - :host { - --md-footnotes-icon: url('data:image/svg+xml;charset=utf-8,'); + `, + }), + injectCss({ + // Extensions + css: ` + /* HIGHLIGHT */ + .highlight .md-clipboard:after { + content: unset; } - :host { - --md-details-icon: url('data:image/svg+xml;charset=utf-8,'); + + .highlight .nx { + color: ${isDarkTheme ? '#ff53a3' : '#ec407a'}; } - :host { - --md-tasklist-icon: url('data:image/svg+xml;charset=utf-8,'); - --md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,'); + + /* CODE HILITE */ + .codehilite .gd { + background-color: ${ + isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd' + }; } - `, + + .codehilite .gi { + background-color: ${ + isDarkTheme ? 'rgba(46,160,67,0.65)' : '#dfd' + }; + } + + /* TABBED */ + .tabbed-set>input:nth-child(1):checked~.tabbed-labels>:nth-child(1), + .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2), + .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3), + .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4), + .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5), + .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6), + .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7), + .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8), + .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9), + .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10), + .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11), + .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12), + .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13), + .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14), + .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15), + .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16), + .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17), + .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18), + .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19), + .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20) { + color: var(--md-accent-fg-color); + border-color: var(--md-accent-fg-color); + } + + /* TASK-LIST */ + .task-list-control .task-list-indicator::before { + background-color: ${theme.palette.action.disabledBackground}; + } + .task-list-control [type="checkbox"]:checked + .task-list-indicator:before { + background-color: ${theme.palette.success.main}; + } + + /* ADMONITION */ + .admonition { + font-size: var(--md-typeset-font-size) !important; + } + .admonition .admonition-title { + padding-left: 2.5rem !important; + } + + .admonition .admonition-title:before { + top: 50% !important; + width: 20px !important; + height: 20px !important; + transform: translateY(-50%) !important; + } + `, }), ]), [ - techdocsSanitizer, - techdocsStorageApi, kind, name, namespace, scmIntegrationsApi, - 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, + techdocsSanitizer, + techdocsStorageApi, + theme, isDarkTheme, isPinned, ], @@ -398,7 +753,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { async (transformedElement: Element) => transformer(transformedElement, [ scrollIntoAnchor(), - copyToClipboard(), + copyToClipboard(theme), addLinkClickListener({ baseUrl: window.location.origin, onClick: (event: MouseEvent, url: string) => { @@ -447,7 +802,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { }, }), ]), - [navigate, techdocsStorageApi], + [theme, navigate, techdocsStorageApi], ); useEffect(() => { @@ -490,7 +845,7 @@ const TheReader = ({ entityRef, onReady = () => {}, withSearch = true, -}: Props) => { +}: ReaderProps) => { const classes = useStyles(); const dom = useTechDocsReaderDom(entityRef); const shadowDomRef = useRef(null); @@ -527,16 +882,20 @@ const TheReader = ({ ); }; -export const Reader = ({ - entityRef, - onReady = () => {}, - withSearch = true, -}: Props) => ( - - - -); +/** + * Component responsible for rendering TechDocs documentation + * + * @public + */ +export const Reader = (props: ReaderProps) => { + const { entityRef, onReady = () => {}, withSearch = true } = props; + return ( + + + + ); +}; diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage.test.tsx similarity index 94% rename from plugins/techdocs/src/reader/components/TechDocsPage.test.tsx rename to plugins/techdocs/src/reader/components/TechDocsReaderPage.test.tsx index b297466a62..55637806a3 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { TechDocsPage } from './TechDocsPage'; +import { TechDocsReaderPage } from './TechDocsReaderPage'; import { render, act } from '@testing-library/react'; import { ConfigReader } from '@backstage/config'; import { @@ -40,10 +40,10 @@ jest.mock('react-router-dom', () => { }; }); -jest.mock('./TechDocsPageHeader', () => { +jest.mock('./TechDocsReaderPageHeader', () => { return { __esModule: true, - TechDocsPageHeader: () =>
, + TechDocsReaderPageHeader: () =>
, }; }); @@ -51,7 +51,7 @@ const { useParams }: { useParams: jest.Mock } = jest.requireMock('react-router-dom'); global.scroll = jest.fn(); -describe('', () => { +describe('', () => { it('should render techdocs page', async () => { useParams.mockReturnValue({ entityRef: 'Component::backstage', @@ -101,7 +101,7 @@ describe('', () => { const rendered = render( wrapInTestApp( - + , ), ); @@ -158,7 +158,7 @@ describe('', () => { const rendered = render( wrapInTestApp( - + {({ techdocsMetadataValue }) => (
', () => { subtitle={techdocsMetadataValue?.site_name} /> )} - + , ), ); diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx similarity index 72% rename from plugins/techdocs/src/reader/components/TechDocsPage.tsx rename to plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx index aae88d406e..d03ea2ef61 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx @@ -19,14 +19,18 @@ import { useOutlet } from 'react-router'; import { useParams } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; import { techdocsApiRef } from '../../api'; -import { TechDocsNotFound } from './TechDocsNotFound'; import { LegacyTechDocsPage } from './LegacyTechDocsPage'; import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types'; import { EntityName } from '@backstage/catalog-model'; -import { useApi } from '@backstage/core-plugin-api'; +import { useApi, useApp } from '@backstage/core-plugin-api'; import { Page } from '@backstage/core-components'; -export type TechDocsPageRenderFunction = ({ +/** + * Helper function that gives the children of {@link TechDocsReaderPage} acccess to techdocs and entity metadata + * + * @public + */ +export type TechDocsReaderPageRenderFunction = ({ techdocsMetadataValue, entityMetadataValue, entityRef, @@ -37,11 +41,18 @@ export type TechDocsPageRenderFunction = ({ onReady: () => void; }) => JSX.Element; -export type TechDocsPageProps = { - children?: TechDocsPageRenderFunction | React.ReactNode; +/** + * Props for {@link TechDocsReaderPage} + * + * @public + */ +export type TechDocsReaderPageProps = { + children?: TechDocsReaderPageRenderFunction | React.ReactNode; }; -export const TechDocsPage = ({ children }: TechDocsPageProps) => { +export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { + const { children } = props; + const { NotFoundErrorPage } = useApp().getComponents(); const outlet = useOutlet(); const [documentReady, setDocumentReady] = useState(false); @@ -66,9 +77,7 @@ export const TechDocsPage = ({ children }: TechDocsPageProps) => { setDocumentReady(true); }, [setDocumentReady]); - if (entityMetadataError) { - return ; - } + if (entityMetadataError) return ; if (!children) return outlet || ; @@ -85,3 +94,16 @@ export const TechDocsPage = ({ children }: TechDocsPageProps) => { ); }; + +/** + * @public + * @deprecated use {@link TechDocsReaderPage} instead + */ +export const TechDocsPage = TechDocsReaderPage; + +/** + * @public + * @deprecated use {@link TechDocsReaderPageRenderFunction} instead + */ + +export type TechDocsPageRenderFunction = TechDocsReaderPageRenderFunction; diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.test.tsx similarity index 93% rename from plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx rename to plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.test.tsx index 4fa43bfed5..9245bb66c5 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.test.tsx @@ -14,17 +14,17 @@ * limitations under the License. */ import React from 'react'; -import { TechDocsPageHeader } from './TechDocsPageHeader'; +import { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader'; import { act } from '@testing-library/react'; import { renderInTestApp } from '@backstage/test-utils'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { rootRouteRef } from '../../routes'; -describe('', () => { +describe('', () => { it('should render a techdocs page header', async () => { await act(async () => { const rendered = await renderInTestApp( - ', () => { it('should render a techdocs page header even if metadata is missing', async () => { await act(async () => { const rendered = await renderInTestApp( - ', () => { it('should render a link back to the component page', async () => { await act(async () => { const rendered = await renderInTestApp( - ; -export const TechDocsPageHeader = ({ - entityRef, - entityMetadata, - techDocsMetadata, -}: TechDocsPageHeaderProps) => { +/** + * Component responsible for rendering a Header with metadata on TechDocs reader page. + * + * @public + */ +export const TechDocsReaderPageHeader = ( + props: TechDocsReaderPageHeaderProps, +) => { + const { entityRef, entityMetadata, techDocsMetadata, children } = props; const { name } = entityRef; const { site_name: siteName, site_description: siteDescription } = @@ -107,6 +118,20 @@ export const TechDocsPageHeader = ({ typeLink={docsRootLink} > {labels} + {children}
); }; + +/** + * @public + * @deprecated use {@link TechDocsReaderPageHeader} instead + */ +export const TechDocsPageHeader = TechDocsReaderPageHeader; + +/** + * @public + * @deprecated use {@link TechDocsReaderPageHeader} instead + */ + +export type TechDocsPageHeaderProps = TechDocsReaderPageHeaderProps; diff --git a/plugins/techdocs/src/reader/components/TechDocsStateIndicator.tsx b/plugins/techdocs/src/reader/components/TechDocsStateIndicator.tsx index e70c17d14a..417dd5727c 100644 --- a/plugins/techdocs/src/reader/components/TechDocsStateIndicator.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsStateIndicator.tsx @@ -23,7 +23,10 @@ import { TechDocsBuildLogs } from './TechDocsBuildLogs'; import { TechDocsNotFound } from './TechDocsNotFound'; import { useTechDocsReader } from './Reader'; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ + root: { + marginBottom: theme.spacing(2), + }, message: { // `word-break: break-word` is deprecated, but gives legacy support to browsers not supporting `overflow-wrap` yet // https://developer.mozilla.org/en-US/docs/Web/CSS/word-break @@ -58,6 +61,7 @@ export const TechDocsStateIndicator = () => { if (state === 'INITIAL_BUILD') { StateAlert = ( } @@ -76,6 +80,7 @@ export const TechDocsStateIndicator = () => { severity="info" icon={} action={} + classes={{ root: classes.root }} > A newer version of this documentation is being prepared and will be available shortly. @@ -93,6 +98,7 @@ export const TechDocsStateIndicator = () => { Refresh } + classes={{ root: classes.root }} > A newer version of this documentation is now available, please refresh to view. @@ -106,7 +112,7 @@ export const TechDocsStateIndicator = () => { variant="outlined" severity="error" action={} - classes={{ message: classes.message }} + classes={{ root: classes.root, message: classes.message }} > Building a newer version of this documentation failed.{' '} {syncErrorMessage} @@ -122,7 +128,7 @@ export const TechDocsStateIndicator = () => { variant="outlined" severity="error" action={} - classes={{ message: classes.message }} + classes={{ root: classes.root, message: classes.message }} > Building a newer version of this documentation failed.{' '} {syncErrorMessage} diff --git a/plugins/techdocs/src/reader/components/index.ts b/plugins/techdocs/src/reader/components/index.ts index 99a0a7a3bc..3124a92f93 100644 --- a/plugins/techdocs/src/reader/components/index.ts +++ b/plugins/techdocs/src/reader/components/index.ts @@ -15,8 +15,13 @@ */ export * from './Reader'; -export * from './TechDocsPage'; -export * from './TechDocsPageHeader'; +export type { + TechDocsReaderPageProps, + TechDocsPageRenderFunction, + TechDocsReaderPageRenderFunction, +} from './TechDocsReaderPage'; +export { TechDocsPage } from './TechDocsReaderPage'; +export * from './TechDocsReaderPageHeader'; export * from './TechDocsStateIndicator'; /** @@ -31,4 +36,3 @@ export * from './TechDocsStateIndicator'; * todo: Make public or stop exporting (ctrl+f "altReaderExperiments") * @internal */ -export { TechDocsSearch } from './TechDocsSearch'; diff --git a/plugins/techdocs/src/reader/index.tsx b/plugins/techdocs/src/reader/index.ts similarity index 100% rename from plugins/techdocs/src/reader/index.tsx rename to plugins/techdocs/src/reader/index.ts diff --git a/plugins/techdocs/src/reader/transformers/addSidebarToggle.ts b/plugins/techdocs/src/reader/transformers/addSidebarToggle.ts new file mode 100644 index 0000000000..9d9db15e45 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/addSidebarToggle.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 type { Transformer } from './index'; +import MenuIcon from '@material-ui/icons/Menu'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +export const addSidebarToggle = (): Transformer => { + return dom => { + // attempting to use selectors that are more likely to be static as MkDocs updates over time + const mkdocsToggleSidebar = dom.querySelector( + '.md-header label[for="__drawer"]', + ) as HTMLLabelElement; + const article = dom.querySelector('article') as HTMLElement; + + // Fail gracefully + if (!mkdocsToggleSidebar || !article) { + return dom; + } + + const toggleSidebar = mkdocsToggleSidebar.cloneNode() as HTMLLabelElement; + ReactDOM.render(React.createElement(MenuIcon), toggleSidebar); + toggleSidebar.style.paddingLeft = '5px'; + toggleSidebar.classList.add('md-content__button'); + toggleSidebar.title = 'Toggle Sidebar'; + toggleSidebar.id = 'toggle-sidebar'; + article?.prepend(toggleSidebar); + return dom; + }; +}; diff --git a/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts b/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts index daa074f6e4..4613df374d 100644 --- a/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts +++ b/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts @@ -16,6 +16,8 @@ import { createTestShadowDom } from '../../test-utils'; import { copyToClipboard } from './copyToClipboard'; +import { lightTheme } from '@backstage/theme'; +import { waitFor } from '@testing-library/react'; const clipboardSpy = jest.fn(); Object.defineProperty(navigator, 'clipboard', { @@ -38,12 +40,17 @@ describe('copyToClipboard', () => { `, { preTransformers: [], - postTransformers: [copyToClipboard()], + postTransformers: [copyToClipboard(lightTheme)], }, ); shadowDom.querySelector('button')?.click(); + await waitFor(() => { + const tooltip = document.querySelector('[role="tooltip"]'); + expect(tooltip).toHaveTextContent('Copied to clipboard'); + }); + expect(clipboardSpy).toHaveBeenCalledWith(expectedClipboard); }); @@ -60,7 +67,7 @@ describe('copyToClipboard', () => { `, { preTransformers: [], - postTransformers: [copyToClipboard()], + postTransformers: [copyToClipboard(lightTheme)], }, ); diff --git a/plugins/techdocs/src/reader/transformers/copyToClipboard.ts b/plugins/techdocs/src/reader/transformers/copyToClipboard.ts deleted file mode 100644 index d0569c0fd9..0000000000 --- a/plugins/techdocs/src/reader/transformers/copyToClipboard.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Transformer } from './transformer'; - -/** - * Recreates copy-to-clipboard functionality attached to snippets that - * is native to mkdocs-material theme. - */ -export const copyToClipboard = (): Transformer => { - return dom => { - Array.from(dom.querySelectorAll('pre > code')).forEach(codeElem => { - const button = document.createElement('button'); - const toBeCopied = codeElem.textContent || ''; - button.className = 'md-clipboard md-icon'; - button.title = 'Copy to clipboard'; - button.innerHTML = - ''; - button.addEventListener('click', () => - navigator.clipboard.writeText(toBeCopied), - ); - codeElem?.parentElement?.prepend(button); - }); - return dom; - }; -}; diff --git a/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx b/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx new file mode 100644 index 0000000000..25a1d67f65 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx @@ -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 React, { useState, useCallback } from 'react'; +import ReactDom from 'react-dom'; +import { + withStyles, + Theme, + ThemeProvider, + SvgIcon, + Tooltip, +} from '@material-ui/core'; + +const CopyToClipboardTooltip = withStyles(theme => ({ + tooltip: { + fontSize: 'inherit', + color: theme.palette.text.primary, + margin: 0, + padding: theme.spacing(0.5), + backgroundColor: 'transparent', + boxShadow: 'none', + }, +}))(Tooltip); + +const CopyToClipboardIcon = () => ( + + + +); + +type CopyToClipboardButtonProps = { + text: string; +}; + +const CopyToClipboardButton = ({ text }: CopyToClipboardButtonProps) => { + const [open, setOpen] = useState(false); + + const handleClick = useCallback(() => { + navigator.clipboard.writeText(text); + setOpen(true); + }, [text]); + + const handleClose = useCallback(() => { + setOpen(false); + }, [setOpen]); + + return ( + + + + ); +}; + +import type { Transformer } from './transformer'; + +/** + * Recreates copy-to-clipboard functionality attached to snippets that + * is native to mkdocs-material theme. + */ +export const copyToClipboard = (theme: Theme): Transformer => { + return dom => { + const codes = dom.querySelectorAll('pre > code'); + for (const code of codes) { + const text = code.textContent || ''; + const container = document.createElement('div'); + code?.parentElement?.prepend(container); + ReactDom.render( + + + , + container, + ); + } + return dom; + }; +}; diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index aa0fba2a18..dcd79180a7 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -16,6 +16,7 @@ export * from './addBaseUrl'; export * from './addGitFeedbackLink'; +export * from './addSidebarToggle'; export * from './rewriteDocLinks'; export * from './addLinkClickListener'; export * from './copyToClipboard'; diff --git a/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.test.ts b/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.test.ts index 020befd522..fb4044f730 100644 --- a/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.test.ts +++ b/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.test.ts @@ -27,10 +27,22 @@ describe('simplifyMkdocsFooter', () => { }, ); - expect(shadowDom.querySelector('.md-footer-copyright')).toBeTruthy(); + expect(shadowDom.querySelector('.md-footer .md-copyright')).toBeTruthy(); }); - it('does remove mkdocs copyright', async () => { + it('does remove new mkdocs copyright', async () => { + const shadowDom = await createTestShadowDom( + FIXTURES.FIXTURE_STANDARD_PAGE, + { + preTransformers: [simplifyMkdocsFooter()], + postTransformers: [], + }, + ); + + expect(shadowDom.querySelector('.md-footer .md-copyright')).toBeFalsy(); + }); + + it('does remove old mkdocs copyright', async () => { const shadowDom = await createTestShadowDom( FIXTURES.FIXTURE_STANDARD_PAGE, { diff --git a/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.ts b/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.ts index 2b96926270..505c88dc71 100644 --- a/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.ts +++ b/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.ts @@ -18,9 +18,10 @@ import type { Transformer } from './transformer'; export const simplifyMkdocsFooter = (): Transformer => { return dom => { - // Remove mkdocs copyright + // Remove new mkdocs copyright + dom.querySelector('.md-footer .md-copyright')?.remove(); + // Remove old mkdocs copyright dom.querySelector('.md-footer-copyright')?.remove(); - return dom; }; }; diff --git a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx b/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx similarity index 66% rename from plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx rename to plugins/techdocs/src/search/components/TechDocsSearch.test.tsx index 5b3d714a04..06d5ac6aac 100644 --- a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx @@ -23,7 +23,7 @@ import { waitFor, within, } from '@testing-library/react'; -import React from 'react'; +import React, { useState } from 'react'; import { TechDocsSearch } from './TechDocsSearch'; const entityId = { @@ -48,7 +48,7 @@ const singleResult = Promise.resolve({ ], }); -describe('', () => { +describe('', () => { it('should render techdocs search bar', async () => { const query = () => emptyResults; const querySpy = jest.fn(query); @@ -70,6 +70,7 @@ describe('', () => { expect(rendered.getByTestId('techdocs-search-bar')).toBeInTheDocument(); }); }); + it('should trigger query when autocomplete input changed', async () => { const query = () => singleResult; const querySpy = jest.fn(query); @@ -119,4 +120,61 @@ describe('', () => { ); }); }); + + it('should update filter values when a new entityName is provided', async () => { + const query = () => singleResult; + const querySpy = jest.fn(query); + const searchApi = { query: querySpy }; + const apiRegistry = TestApiRegistry.from([searchApiRef, searchApi]); + const newEntityId = { + name: 'test-diff', + namespace: 'testspace-diff', + kind: 'TestableDiff', + }; + + const WrappedSearchBar = () => { + const [entityName, setEntityName] = useState(entityId); + return wrapInTestApp( + + + + , + ); + }; + + await act(async () => { + const rendered = render(); + + await singleResult; + expect(querySpy).toBeCalledWith({ + filters: { + kind: 'Testable', + name: 'test', + namespace: 'testspace', + }, + pageCursor: '', + term: '', + types: ['techdocs'], + }); + + const button = rendered.getByText('Update Entity'); + button.click(); + + await singleResult; + await waitFor(() => + expect(querySpy).toBeCalledWith({ + filters: { + kind: 'TestableDiff', + name: 'test-diff', + namespace: 'testspace-diff', + }, + pageCursor: '', + term: '', + types: ['techdocs'], + }), + ); + }); + }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsSearch.tsx b/plugins/techdocs/src/search/components/TechDocsSearch.tsx similarity index 52% rename from plugins/techdocs/src/reader/components/TechDocsSearch.tsx rename to plugins/techdocs/src/search/components/TechDocsSearch.tsx index 0760eb47f8..c817fb3217 100644 --- a/plugins/techdocs/src/reader/components/TechDocsSearch.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearch.tsx @@ -17,8 +17,8 @@ import { EntityName } from '@backstage/catalog-model'; import { SearchContextProvider, useSearch } from '@backstage/plugin-search'; import { + makeStyles, CircularProgress, - Grid, IconButton, InputAdornment, TextField, @@ -28,9 +28,20 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import React, { ChangeEvent, useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; import useDebounce from 'react-use/lib/useDebounce'; -import { DocsResultListItem } from '../../components/DocsResultListItem'; +import { TechDocsSearchResultListItem } from './TechDocsSearchResultListItem'; -type TechDocsSearchProps = { +const useStyles = makeStyles({ + root: { + width: '100%', + }, +}); + +/** + * Props for {@link TechDocsSearch} + * + * @public + */ +export type TechDocsSearchProps = { entityId: EntityName; debounceTime?: number; }; @@ -49,17 +60,17 @@ type TechDocsSearchResult = { document: TechDocsDoc; }; -const TechDocsSearchBar = ({ - entityId, - debounceTime = 150, -}: TechDocsSearchProps) => { +const TechDocsSearchBar = (props: TechDocsSearchProps) => { + const { entityId, debounceTime = 150 } = props; const [open, setOpen] = useState(false); const navigate = useNavigate(); const { term, setTerm, + setFilters, result: { loading, value: searchVal }, } = useSearch(); + const classes = useStyles(); const [options, setOptions] = useState([]); useEffect(() => { let mounted = true; @@ -80,6 +91,20 @@ const TechDocsSearchBar = ({ useDebounce(() => setTerm(value), debounceTime, [value]); + // Update the filter context when the entityId changes, e.g. when the search + // bar continues to be rendered, navigating between different TechDocs sites. + const { kind, name, namespace } = entityId; + useEffect(() => { + setFilters(prevFilters => { + return { + ...prevFilters, + kind, + namespace, + name, + }; + }); + }, [kind, namespace, name, setFilters]); + const handleQuery = (e: ChangeEvent) => { if (!open) { setOpen(true); @@ -95,70 +120,74 @@ const TechDocsSearchBar = ({ }; return ( - - ''} - filterOptions={x => { - return x; // This is needed to get renderOption to be called after options change. Bug in material-ui? - }} - onClose={() => { - setOpen(false); - }} - onFocus={() => { - setOpen(true); - }} - onChange={handleSelection} - blurOnSelect - noOptionsText="No results found" - value={null} - options={options} - renderOption={({ document }) => ( - - )} - loading={loading} - renderInput={params => ( - - - - - - ), - endAdornment: ( - - {loading ? ( - - ) : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} - /> - + ''} + filterOptions={x => { + return x; // This is needed to get renderOption to be called after options change. Bug in material-ui? + }} + onClose={() => { + setOpen(false); + }} + onFocus={() => { + setOpen(true); + }} + onChange={handleSelection} + blurOnSelect + noOptionsText="No results found" + value={null} + options={options} + renderOption={({ document }) => ( + + )} + loading={loading} + renderInput={params => ( + + + + + + ), + endAdornment: ( + + {loading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> ); }; +/** + * Component used to render search bar on TechDocs page, scoped to + * + * @public + */ export const TechDocsSearch = (props: TechDocsSearchProps) => { const initialState = { term: '', diff --git a/plugins/techdocs/src/components/DocsResultListItem/DocsResultListItem.test.tsx b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.test.tsx similarity index 84% rename from plugins/techdocs/src/components/DocsResultListItem/DocsResultListItem.test.tsx rename to plugins/techdocs/src/search/components/TechDocsSearchResultListItem.test.tsx index 807914c5ad..7ebdd6c340 100644 --- a/plugins/techdocs/src/components/DocsResultListItem/DocsResultListItem.test.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { DocsResultListItem } from './DocsResultListItem'; +import { TechDocsSearchResultListItem } from './TechDocsSearchResultListItem'; // Using canvas to render text.. jest.mock('react-text-truncate', () => { @@ -44,9 +44,11 @@ const validResultWithTitle = { lifecycle: 'production', }; -describe('DocsResultListItem test', () => { +describe('TechDocsSearchResultListItem test', () => { it('should render search doc passed in', async () => { - const { findByText } = render(); + const { findByText } = render( + , + ); expect( await findByText('Documentation | Backstage docs'), @@ -60,7 +62,10 @@ describe('DocsResultListItem test', () => { it('should use title if defined', async () => { const { findByText } = render( - , + , ); expect(await findByText('Count Dookumentation')).toBeInTheDocument(); @@ -73,7 +78,7 @@ describe('DocsResultListItem test', () => { it('should use entity title if defined', async () => { const { findByText } = render( - , + , ); expect( diff --git a/plugins/techdocs/src/components/DocsResultListItem/DocsResultListItem.tsx b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx similarity index 78% rename from plugins/techdocs/src/components/DocsResultListItem/DocsResultListItem.tsx rename to plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx index 03cf011c32..152fa2e3d3 100644 --- a/plugins/techdocs/src/components/DocsResultListItem/DocsResultListItem.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx @@ -29,19 +29,34 @@ const useStyles = makeStyles({ }, }); -export const DocsResultListItem = ({ - result, - lineClamp = 5, - asListItem = true, - asLink = true, - title, -}: { +/** + * Props for {@link TechDocsSearchResultListItem}. + * + * @public + */ +export type TechDocsSearchResultListItemProps = { result: any; lineClamp?: number; asListItem?: boolean; asLink?: boolean; title?: string; -}) => { +}; + +/** + * Component which renders documentation and related metadata. + * + * @public + */ +export const TechDocsSearchResultListItem = ( + props: TechDocsSearchResultListItemProps, +) => { + const { + result, + lineClamp = 5, + asListItem = true, + asLink = true, + title, + } = props; const classes = useStyles(); const TextItem = () => ( ); }; + +/** + * @public + * @deprecated use {@link TechDocsSearchResultListItem} instead + */ +export const DocsResultListItem = TechDocsSearchResultListItem; diff --git a/plugins/catalog/src/components/CatalogResultListItem/index.ts b/plugins/techdocs/src/search/components/index.ts similarity index 88% rename from plugins/catalog/src/components/CatalogResultListItem/index.ts rename to plugins/techdocs/src/search/components/index.ts index e8a67d8f89..1f7615b4ff 100644 --- a/plugins/catalog/src/components/CatalogResultListItem/index.ts +++ b/plugins/techdocs/src/search/components/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export { CatalogResultListItem } from './CatalogResultListItem'; +export * from './TechDocsSearchResultListItem'; +export * from './TechDocsSearch'; diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/index.ts b/plugins/techdocs/src/search/index.ts similarity index 94% rename from plugins/catalog/src/components/EntityPageLayout/Tabbed/index.ts rename to plugins/techdocs/src/search/index.ts index ff14fe3b2c..6f9ea2a52d 100644 --- a/plugins/catalog/src/components/EntityPageLayout/Tabbed/index.ts +++ b/plugins/techdocs/src/search/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Tabbed } from './Tabbed'; + +export * from './components'; diff --git a/plugins/techdocs/src/test-utils/fixtures/mkdocs-index.ts b/plugins/techdocs/src/test-utils/fixtures/mkdocs-index.ts index afb84dc10b..21d703fa2e 100644 --- a/plugins/techdocs/src/test-utils/fixtures/mkdocs-index.ts +++ b/plugins/techdocs/src/test-utils/fixtures/mkdocs-index.ts @@ -1520,7 +1520,7 @@ css img js mkdocs sitemap.xml