diff --git a/.changeset/breezy-bees-care.md b/.changeset/breezy-bees-care.md new file mode 100644 index 0000000000..9a5b59fe03 --- /dev/null +++ b/.changeset/breezy-bees-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +Add `queryEntities` method to `CatalogApi`. diff --git a/.changeset/bright-kids-raise.md b/.changeset/bright-kids-raise.md new file mode 100644 index 0000000000..e0d69e0c71 --- /dev/null +++ b/.changeset/bright-kids-raise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-todo-backend': patch +--- + +todo-backend is now exposed as a plugin which uses the new plugin system diff --git a/.changeset/brown-islands-own.md b/.changeset/brown-islands-own.md deleted file mode 100644 index d91ab89fd8..0000000000 --- a/.changeset/brown-islands-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-azure-devops': patch ---- - -Fixed `AssignedToCurrentUsersTeams` & `CreatedByCurrentUsersTeams` filter in `AzurePullRequestsPage` component. diff --git a/.changeset/clean-planes-join.md b/.changeset/clean-planes-join.md new file mode 100644 index 0000000000..dd8280f553 --- /dev/null +++ b/.changeset/clean-planes-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Changed the `createPermissionIntegrationRouter` API to allow `getResources`, `resourceType` and `rules` to be optional diff --git a/.changeset/cool-clocks-prove.md b/.changeset/cool-clocks-prove.md new file mode 100644 index 0000000000..dfd7b8c5eb --- /dev/null +++ b/.changeset/cool-clocks-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed a bug where additional backend package entry points where not properly marked as internal during local development. diff --git a/.changeset/create-app-1676993958.md b/.changeset/create-app-1676993958.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1676993958.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/curvy-pets-hang.md b/.changeset/curvy-pets-hang.md new file mode 100644 index 0000000000..3a2d11ac12 --- /dev/null +++ b/.changeset/curvy-pets-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Tweaked type dependency check to trim wildcard type imports. diff --git a/.changeset/eight-falcons-explode.md b/.changeset/eight-falcons-explode.md deleted file mode 100644 index d9a1a2fe1f..0000000000 --- a/.changeset/eight-falcons-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-app-api': patch ---- - -Fixed an issue were the log redaction didn't properly escape RegExp characters. diff --git a/.changeset/eight-radios-bake.md b/.changeset/eight-radios-bake.md new file mode 100644 index 0000000000..173739a625 --- /dev/null +++ b/.changeset/eight-radios-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +throw error from catalog:fetch scaffolder action when entity is null and optional is false diff --git a/.changeset/eighty-geese-return.md b/.changeset/eighty-geese-return.md new file mode 100644 index 0000000000..49361de8e7 --- /dev/null +++ b/.changeset/eighty-geese-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Revert using `baseUrl` for MS Graph provider as it breaks token retrieval diff --git a/.changeset/flat-cups-itch.md b/.changeset/flat-cups-itch.md deleted file mode 100644 index 250393c21a..0000000000 --- a/.changeset/flat-cups-itch.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder-react': patch -'@backstage/plugin-scaffolder': patch ---- - -lock versions of `@rjsf/*-beta` packages diff --git a/.changeset/flat-kids-occur.md b/.changeset/flat-kids-occur.md new file mode 100644 index 0000000000..4e4c002928 --- /dev/null +++ b/.changeset/flat-kids-occur.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': minor +'@backstage/plugin-scaffolder': minor +--- + +Move `useTaskStream`, `TaskBorder`, `TaskLogStream` and `TaskSteps` into `scaffolder-react`. diff --git a/.changeset/flat-peaches-act.md b/.changeset/flat-peaches-act.md new file mode 100644 index 0000000000..e4b4138ee5 --- /dev/null +++ b/.changeset/flat-peaches-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Techdocs backend explains a bit more about what might have caused the docs not being found when building techdocs. diff --git a/.changeset/forty-snails-clap.md b/.changeset/forty-snails-clap.md new file mode 100644 index 0000000000..f5aef12d9d --- /dev/null +++ b/.changeset/forty-snails-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': minor +--- + +Added endpoint to get a list of known incremental entity providers diff --git a/.changeset/four-lizards-grin.md b/.changeset/four-lizards-grin.md new file mode 100644 index 0000000000..6fa5f57243 --- /dev/null +++ b/.changeset/four-lizards-grin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix export of `defaultCatalogCollatorEntityTransformer`. diff --git a/.changeset/fuzzy-trains-search.md b/.changeset/fuzzy-trains-search.md new file mode 100644 index 0000000000..229d25f3b5 --- /dev/null +++ b/.changeset/fuzzy-trains-search.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fix icon alignment in `CatalogSearchResultListItem` diff --git a/.changeset/gentle-pears-clean.md b/.changeset/gentle-pears-clean.md new file mode 100644 index 0000000000..d92c3709ae --- /dev/null +++ b/.changeset/gentle-pears-clean.md @@ -0,0 +1,5 @@ +--- +'@backstage/eslint-plugin': patch +--- + +Fixing a bug that we should check internal dependencies too diff --git a/.changeset/great-trains-jam.md b/.changeset/great-trains-jam.md new file mode 100644 index 0000000000..91133bfe32 --- /dev/null +++ b/.changeset/great-trains-jam.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fix bug that caused next and previous links not to work with certain versions of mkdocs-material diff --git a/.changeset/grumpy-bikes-begin.md b/.changeset/grumpy-bikes-begin.md new file mode 100644 index 0000000000..a98e6651f5 --- /dev/null +++ b/.changeset/grumpy-bikes-begin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights-backend': patch +--- + +Add DB index to improve latency of latest fact query diff --git a/.changeset/happy-boxes-arrive.md b/.changeset/happy-boxes-arrive.md new file mode 100644 index 0000000000..85998c1bdd --- /dev/null +++ b/.changeset/happy-boxes-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +fix bug with `hasErrors` returning false when dealing with empty objects diff --git a/.changeset/honest-nails-bake.md b/.changeset/honest-nails-bake.md new file mode 100644 index 0000000000..03d5a080c9 --- /dev/null +++ b/.changeset/honest-nails-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-octopus-deploy': minor +--- + +Initial version diff --git a/.changeset/khaki-poems-run.md b/.changeset/khaki-poems-run.md new file mode 100644 index 0000000000..74db902c02 --- /dev/null +++ b/.changeset/khaki-poems-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-validation': patch +--- + +Fixed bug in EntityVaidation plugin where string in text box was unable to be updated. diff --git a/.changeset/lazy-badgers-rule.md b/.changeset/lazy-badgers-rule.md deleted file mode 100644 index 748b9dbadc..0000000000 --- a/.changeset/lazy-badgers-rule.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-tech-insights-backend-module-jsonfc': patch ---- - -Surface the cause of the json rules engine diff --git a/.changeset/light-sheep-trade.md b/.changeset/light-sheep-trade.md new file mode 100644 index 0000000000..480ffc973e --- /dev/null +++ b/.changeset/light-sheep-trade.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated the ESLint plugin configuration that is enabled through `yarn start --check` to only pick up valid source files. diff --git a/.changeset/long-nails-pump.md b/.changeset/long-nails-pump.md new file mode 100644 index 0000000000..d9eefc0249 --- /dev/null +++ b/.changeset/long-nails-pump.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added a new `migrate package-exports` command that synchronizes package exports fields in all `package.json`s. diff --git a/.changeset/long-wolves-drive.md b/.changeset/long-wolves-drive.md new file mode 100644 index 0000000000..1c3ffadb5a --- /dev/null +++ b/.changeset/long-wolves-drive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-octopus-deploy': patch +--- + +Added Octopus Deploy logo diff --git a/.changeset/metal-suns-rhyme.md b/.changeset/metal-suns-rhyme.md new file mode 100644 index 0000000000..3fefdca7c6 --- /dev/null +++ b/.changeset/metal-suns-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Bumped the `msw` version in templates to 1.0.0 diff --git a/.changeset/new-jobs-deny.md b/.changeset/new-jobs-deny.md new file mode 100644 index 0000000000..e7b5bff71a --- /dev/null +++ b/.changeset/new-jobs-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Add possibility to re-use EntityPicker for filters with multiple select. diff --git a/.changeset/nice-planets-wave.md b/.changeset/nice-planets-wave.md new file mode 100644 index 0000000000..2af1a14d83 --- /dev/null +++ b/.changeset/nice-planets-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist-backend': minor +--- + +**BREAKING** The linguist-backend `createRouter` now requires that the `tokenManger` is passed to the router. diff --git a/.changeset/ninety-turtles-wait.md b/.changeset/ninety-turtles-wait.md new file mode 100644 index 0000000000..683583528f --- /dev/null +++ b/.changeset/ninety-turtles-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +The HTML tag attributes in the documentation content inserted to shadow DOM is preserved to improve accessibility diff --git a/.changeset/odd-fireants-bathe.md b/.changeset/odd-fireants-bathe.md new file mode 100644 index 0000000000..d4b7f5dc5a --- /dev/null +++ b/.changeset/odd-fireants-bathe.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-adr-backend': patch +'@backstage/cli': patch +--- + +Adjust express dependencies to be the same as the rest of the project diff --git a/.changeset/odd-waves-rescue.md b/.changeset/odd-waves-rescue.md new file mode 100644 index 0000000000..e435bc9962 --- /dev/null +++ b/.changeset/odd-waves-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Create an "Other Templates" group when groups are given to scaffolder/next. diff --git a/.changeset/old-foxes-shave.md b/.changeset/old-foxes-shave.md new file mode 100644 index 0000000000..4cd0808103 --- /dev/null +++ b/.changeset/old-foxes-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Add ApiDefinitionDialog component for fast access to the API definition in ProvidedApiCards and ConsumedApiCards. diff --git a/.changeset/perfect-cheetahs-serve.md b/.changeset/perfect-cheetahs-serve.md deleted file mode 100644 index 08f7c1da21..0000000000 --- a/.changeset/perfect-cheetahs-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor ---- - -Added the option to overwrite files in the `targetPath` of the `template:fetch` action diff --git a/.changeset/perfect-mayflies-greet.md b/.changeset/perfect-mayflies-greet.md new file mode 100644 index 0000000000..7e57507dae --- /dev/null +++ b/.changeset/perfect-mayflies-greet.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated frontend plugin template to use some more recent features diff --git a/.changeset/pink-falcons-serve.md b/.changeset/pink-falcons-serve.md deleted file mode 100644 index 8cc6859c25..0000000000 --- a/.changeset/pink-falcons-serve.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder': minor -'@backstage/plugin-scaffolder-react': minor ---- - -Embed scaffolder workflow in other components diff --git a/.changeset/polite-chicken-do.md b/.changeset/polite-chicken-do.md new file mode 100644 index 0000000000..33e4149253 --- /dev/null +++ b/.changeset/polite-chicken-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-vault-backend': patch +--- + +Ignore the `eslint` error diff --git a/.changeset/polite-falcons-jump.md b/.changeset/polite-falcons-jump.md new file mode 100644 index 0000000000..ab9209913d --- /dev/null +++ b/.changeset/polite-falcons-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Fix small UI-glitch in MemberListCard diff --git a/.changeset/polite-wombats-smash.md b/.changeset/polite-wombats-smash.md new file mode 100644 index 0000000000..98569e1799 --- /dev/null +++ b/.changeset/polite-wombats-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/errors': patch +--- + +Added `NotImplementedError`, which can be used when the server does not recognize the request method and is incapable of supporting it for any resource. diff --git a/.changeset/pre.json b/.changeset/pre.json index 9f9a48e8dc..d3d0c1d42d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,199 +2,253 @@ "mode": "pre", "tag": "next", "initialVersions": { - "example-app": "0.2.79", - "@backstage/app-defaults": "1.1.0", - "example-backend": "0.2.79", - "@backstage/backend-app-api": "0.3.0", - "@backstage/backend-common": "0.18.0", - "@backstage/backend-defaults": "0.1.5", - "example-backend-next": "0.0.7", - "@backstage/backend-plugin-api": "0.3.0", - "@backstage/backend-tasks": "0.4.1", - "@backstage/backend-test-utils": "0.1.32", - "@backstage/catalog-client": "1.3.0", - "@backstage/catalog-model": "1.1.5", - "@backstage/cli": "0.22.1", + "example-app": "0.2.80", + "@backstage/app-defaults": "1.2.0", + "example-backend": "0.2.80", + "@backstage/backend-app-api": "0.4.0", + "@backstage/backend-common": "0.18.2", + "@backstage/backend-defaults": "0.1.7", + "@backstage/backend-dev-utils": "0.1.0", + "example-backend-next": "0.0.8", + "@backstage/backend-plugin-api": "0.4.0", + "@backstage/backend-tasks": "0.4.3", + "@backstage/backend-test-utils": "0.1.34", + "@backstage/catalog-client": "1.3.1", + "@backstage/catalog-model": "1.2.0", + "@backstage/cli": "0.22.2", "@backstage/cli-common": "0.1.11", - "@backstage/codemods": "0.1.42", + "@backstage/codemods": "0.1.43", "@backstage/config": "1.0.6", "@backstage/config-loader": "1.1.8", - "@backstage/core-app-api": "1.4.0", - "@backstage/core-components": "0.12.3", - "@backstage/core-plugin-api": "1.3.0", - "@backstage/create-app": "0.4.36", - "@backstage/dev-utils": "1.0.11", + "@backstage/core-app-api": "1.5.0", + "@backstage/core-components": "0.12.4", + "@backstage/core-plugin-api": "1.4.0", + "@backstage/create-app": "0.4.37", + "@backstage/dev-utils": "1.0.12", "e2e-test": "0.2.0", "@backstage/errors": "1.1.4", + "@backstage/eslint-plugin": "0.1.0", "@backstage/integration": "1.4.2", "@backstage/integration-aws-node": "0.1.1", - "@backstage/integration-react": "1.1.9", + "@backstage/integration-react": "1.1.10", "@backstage/release-manifests": "0.0.8", - "@backstage/repo-tools": "0.1.1", - "@techdocs/cli": "1.3.0", - "techdocs-cli-embedded-app": "0.2.78", - "@backstage/test-utils": "1.2.4", - "@backstage/theme": "0.2.16", + "@backstage/repo-tools": "0.1.2", + "@techdocs/cli": "1.3.2", + "techdocs-cli-embedded-app": "0.2.79", + "@backstage/test-utils": "1.2.5", + "@backstage/theme": "0.2.17", "@backstage/types": "1.0.2", "@backstage/version-bridge": "1.0.3", - "@backstage/plugin-adr": "0.3.0", - "@backstage/plugin-adr-backend": "0.2.5", - "@backstage/plugin-adr-common": "0.2.5", - "@backstage/plugin-airbrake": "0.3.14", - "@backstage/plugin-airbrake-backend": "0.2.13", - "@backstage/plugin-allure": "0.1.30", - "@backstage/plugin-analytics-module-ga": "0.1.25", - "@backstage/plugin-apache-airflow": "0.2.7", - "@backstage/plugin-api-docs": "0.8.14", - "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.0", - "@backstage/plugin-apollo-explorer": "0.1.7", - "@backstage/plugin-app-backend": "0.3.40", - "@backstage/plugin-auth-backend": "0.17.3", - "@backstage/plugin-auth-node": "0.2.9", - "@backstage/plugin-azure-devops": "0.2.5", - "@backstage/plugin-azure-devops-backend": "0.3.19", + "@backstage/plugin-adr": "0.4.0", + "@backstage/plugin-adr-backend": "0.3.0", + "@backstage/plugin-adr-common": "0.2.6", + "@backstage/plugin-airbrake": "0.3.15", + "@backstage/plugin-airbrake-backend": "0.2.15", + "@backstage/plugin-allure": "0.1.31", + "@backstage/plugin-analytics-module-ga": "0.1.26", + "@backstage/plugin-apache-airflow": "0.2.8", + "@backstage/plugin-api-docs": "0.9.0", + "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.1", + "@backstage/plugin-apollo-explorer": "0.1.8", + "@backstage/plugin-app-backend": "0.3.42", + "@backstage/plugin-auth-backend": "0.18.0", + "@backstage/plugin-auth-node": "0.2.11", + "@backstage/plugin-azure-devops": "0.2.6", + "@backstage/plugin-azure-devops-backend": "0.3.21", "@backstage/plugin-azure-devops-common": "0.3.0", - "@backstage/plugin-azure-sites": "0.1.3", - "@backstage/plugin-azure-sites-backend": "0.1.2", + "@backstage/plugin-azure-sites": "0.1.4", + "@backstage/plugin-azure-sites-backend": "0.1.4", "@backstage/plugin-azure-sites-common": "0.1.0", - "@backstage/plugin-badges": "0.2.38", - "@backstage/plugin-badges-backend": "0.1.34", - "@backstage/plugin-bazaar": "0.2.3", - "@backstage/plugin-bazaar-backend": "0.2.3", + "@backstage/plugin-badges": "0.2.39", + "@backstage/plugin-badges-backend": "0.1.36", + "@backstage/plugin-bazaar": "0.2.4", + "@backstage/plugin-bazaar-backend": "0.2.5", "@backstage/plugin-bitbucket-cloud-common": "0.2.3", - "@backstage/plugin-bitrise": "0.1.41", - "@backstage/plugin-catalog": "1.7.2", - "@backstage/plugin-catalog-backend": "1.7.0", - "@backstage/plugin-catalog-backend-module-aws": "0.1.13", - "@backstage/plugin-catalog-backend-module-azure": "0.1.11", - "@backstage/plugin-catalog-backend-module-bitbucket": "0.2.7", - "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.7", - "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.5", - "@backstage/plugin-catalog-backend-module-gerrit": "0.1.8", - "@backstage/plugin-catalog-backend-module-github": "0.2.3", - "@backstage/plugin-catalog-backend-module-gitlab": "0.1.11", - "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.1.1", - "@backstage/plugin-catalog-backend-module-ldap": "0.5.7", - "@backstage/plugin-catalog-backend-module-msgraph": "0.4.6", - "@backstage/plugin-catalog-backend-module-openapi": "0.1.6", - "@backstage/plugin-catalog-common": "1.0.10", - "@internal/plugin-catalog-customized": "0.0.6", - "@backstage/plugin-catalog-graph": "0.2.26", - "@backstage/plugin-catalog-graphql": "0.3.17", - "@backstage/plugin-catalog-import": "0.9.4", - "@backstage/plugin-catalog-node": "1.3.1", - "@backstage/plugin-catalog-react": "1.2.4", - "@backstage/plugin-cicd-statistics": "0.1.16", - "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.10", - "@backstage/plugin-circleci": "0.3.14", - "@backstage/plugin-cloudbuild": "0.3.14", - "@backstage/plugin-code-climate": "0.1.14", - "@backstage/plugin-code-coverage": "0.2.7", - "@backstage/plugin-code-coverage-backend": "0.2.6", - "@backstage/plugin-codescene": "0.1.9", - "@backstage/plugin-config-schema": "0.1.37", - "@backstage/plugin-cost-insights": "0.12.3", + "@backstage/plugin-bitrise": "0.1.42", + "@backstage/plugin-catalog": "1.8.0", + "@backstage/plugin-catalog-backend": "1.7.2", + "@backstage/plugin-catalog-backend-module-aws": "0.1.15", + "@backstage/plugin-catalog-backend-module-azure": "0.1.13", + "@backstage/plugin-catalog-backend-module-bitbucket": "0.2.9", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.9", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.7", + "@backstage/plugin-catalog-backend-module-gerrit": "0.1.10", + "@backstage/plugin-catalog-backend-module-github": "0.2.5", + "@backstage/plugin-catalog-backend-module-gitlab": "0.1.13", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.2.0", + "@backstage/plugin-catalog-backend-module-ldap": "0.5.9", + "@backstage/plugin-catalog-backend-module-msgraph": "0.5.0", + "@backstage/plugin-catalog-backend-module-openapi": "0.1.8", + "@backstage/plugin-catalog-common": "1.0.11", + "@internal/plugin-catalog-customized": "0.0.7", + "@backstage/plugin-catalog-graph": "0.2.27", + "@backstage/plugin-catalog-graphql": "0.3.18", + "@backstage/plugin-catalog-import": "0.9.5", + "@backstage/plugin-catalog-node": "1.3.3", + "@backstage/plugin-catalog-react": "1.3.0", + "@backstage/plugin-cicd-statistics": "0.1.17", + "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.11", + "@backstage/plugin-circleci": "0.3.15", + "@backstage/plugin-cloudbuild": "0.3.15", + "@backstage/plugin-code-climate": "0.1.15", + "@backstage/plugin-code-coverage": "0.2.8", + "@backstage/plugin-code-coverage-backend": "0.2.8", + "@backstage/plugin-codescene": "0.1.10", + "@backstage/plugin-config-schema": "0.1.38", + "@backstage/plugin-cost-insights": "0.12.4", "@backstage/plugin-cost-insights-common": "0.1.1", - "@backstage/plugin-dynatrace": "1.0.4", - "@backstage/plugin-events-backend": "0.2.1", - "@backstage/plugin-events-backend-module-aws-sqs": "0.1.2", - "@backstage/plugin-events-backend-module-azure": "0.1.2", - "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.1.2", - "@backstage/plugin-events-backend-module-gerrit": "0.1.2", - "@backstage/plugin-events-backend-module-github": "0.1.2", - "@backstage/plugin-events-backend-module-gitlab": "0.1.2", - "@backstage/plugin-events-backend-test-utils": "0.1.2", - "@backstage/plugin-events-node": "0.2.1", - "@internal/plugin-todo-list": "1.0.9", - "@internal/plugin-todo-list-backend": "1.0.9", + "@backstage/plugin-dynatrace": "2.0.0", + "@backstage/plugin-entity-feedback": "0.1.0", + "@backstage/plugin-entity-feedback-backend": "0.1.0", + "@backstage/plugin-entity-feedback-common": "0.1.0", + "@backstage/plugin-entity-validation": "0.1.0", + "@backstage/plugin-events-backend": "0.2.3", + "@backstage/plugin-events-backend-module-aws-sqs": "0.1.4", + "@backstage/plugin-events-backend-module-azure": "0.1.4", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.1.4", + "@backstage/plugin-events-backend-module-gerrit": "0.1.4", + "@backstage/plugin-events-backend-module-github": "0.1.4", + "@backstage/plugin-events-backend-module-gitlab": "0.1.4", + "@backstage/plugin-events-backend-test-utils": "0.1.4", + "@backstage/plugin-events-node": "0.2.3", + "@internal/plugin-todo-list": "1.0.10", + "@internal/plugin-todo-list-backend": "1.0.10", "@internal/plugin-todo-list-common": "1.0.8", - "@backstage/plugin-explore": "0.3.45", - "@backstage/plugin-explore-backend": "0.0.2", + "@backstage/plugin-explore": "0.4.0", + "@backstage/plugin-explore-backend": "0.0.4", "@backstage/plugin-explore-common": "0.0.1", - "@backstage/plugin-explore-react": "0.0.25", - "@backstage/plugin-firehydrant": "0.1.31", - "@backstage/plugin-fossa": "0.2.46", - "@backstage/plugin-gcalendar": "0.3.10", - "@backstage/plugin-gcp-projects": "0.3.33", - "@backstage/plugin-git-release-manager": "0.3.27", - "@backstage/plugin-github-actions": "0.5.14", - "@backstage/plugin-github-deployments": "0.1.45", - "@backstage/plugin-github-issues": "0.2.3", - "@backstage/plugin-github-pull-requests-board": "0.1.8", - "@backstage/plugin-gitops-profiles": "0.3.32", - "@backstage/plugin-gocd": "0.1.20", - "@backstage/plugin-graphiql": "0.2.46", - "@backstage/plugin-graphql-backend": "0.1.30", - "@backstage/plugin-home": "0.4.30", - "@backstage/plugin-ilert": "0.2.3", - "@backstage/plugin-jenkins": "0.7.13", - "@backstage/plugin-jenkins-backend": "0.1.30", - "@backstage/plugin-jenkins-common": "0.1.12", - "@backstage/plugin-kafka": "0.3.14", - "@backstage/plugin-kafka-backend": "0.2.33", - "@backstage/plugin-kubernetes": "0.7.7", - "@backstage/plugin-kubernetes-backend": "0.9.1", - "@backstage/plugin-kubernetes-common": "0.5.1", - "@backstage/plugin-lighthouse": "0.3.14", - "@backstage/plugin-newrelic": "0.3.32", - "@backstage/plugin-newrelic-dashboard": "0.2.7", - "@backstage/plugin-org": "0.6.4", - "@backstage/plugin-org-react": "0.1.3", - "@backstage/plugin-pagerduty": "0.5.7", - "@backstage/plugin-periskop": "0.1.12", - "@backstage/plugin-periskop-backend": "0.1.11", - "@backstage/plugin-permission-backend": "0.5.15", + "@backstage/plugin-explore-react": "0.0.26", + "@backstage/plugin-firehydrant": "0.1.32", + "@backstage/plugin-fossa": "0.2.47", + "@backstage/plugin-gcalendar": "0.3.11", + "@backstage/plugin-gcp-projects": "0.3.34", + "@backstage/plugin-git-release-manager": "0.3.28", + "@backstage/plugin-github-actions": "0.5.15", + "@backstage/plugin-github-deployments": "0.1.46", + "@backstage/plugin-github-issues": "0.2.4", + "@backstage/plugin-github-pull-requests-board": "0.1.9", + "@backstage/plugin-gitops-profiles": "0.3.33", + "@backstage/plugin-gocd": "0.1.21", + "@backstage/plugin-graphiql": "0.2.47", + "@backstage/plugin-graphql-backend": "0.1.32", + "@backstage/plugin-graphql-voyager": "0.1.0", + "@backstage/plugin-home": "0.4.31", + "@backstage/plugin-ilert": "0.2.4", + "@backstage/plugin-jenkins": "0.7.14", + "@backstage/plugin-jenkins-backend": "0.1.32", + "@backstage/plugin-jenkins-common": "0.1.13", + "@backstage/plugin-kafka": "0.3.15", + "@backstage/plugin-kafka-backend": "0.2.35", + "@backstage/plugin-kubernetes": "0.7.8", + "@backstage/plugin-kubernetes-backend": "0.9.3", + "@backstage/plugin-kubernetes-common": "0.6.0", + "@backstage/plugin-lighthouse": "0.4.0", + "@backstage/plugin-lighthouse-backend": "0.1.0", + "@backstage/plugin-lighthouse-common": "0.1.0", + "@backstage/plugin-linguist": "0.1.0", + "@backstage/plugin-linguist-backend": "0.1.0", + "@backstage/plugin-linguist-common": "0.1.0", + "@backstage/plugin-microsoft-calendar": "0.1.0", + "@backstage/plugin-newrelic": "0.3.33", + "@backstage/plugin-newrelic-dashboard": "0.2.8", + "@backstage/plugin-org": "0.6.5", + "@backstage/plugin-org-react": "0.1.4", + "@backstage/plugin-pagerduty": "0.5.8", + "@backstage/plugin-periskop": "0.1.13", + "@backstage/plugin-periskop-backend": "0.1.13", + "@backstage/plugin-permission-backend": "0.5.17", "@backstage/plugin-permission-common": "0.7.3", - "@backstage/plugin-permission-node": "0.7.3", - "@backstage/plugin-permission-react": "0.4.9", - "@backstage/plugin-playlist": "0.1.5", - "@backstage/plugin-playlist-backend": "0.2.3", + "@backstage/plugin-permission-node": "0.7.5", + "@backstage/plugin-permission-react": "0.4.10", + "@backstage/plugin-playlist": "0.1.6", + "@backstage/plugin-playlist-backend": "0.2.5", "@backstage/plugin-playlist-common": "0.1.4", - "@backstage/plugin-proxy-backend": "0.2.34", - "@backstage/plugin-rollbar": "0.4.14", - "@backstage/plugin-rollbar-backend": "0.1.37", - "@backstage/plugin-scaffolder": "1.10.0", - "@backstage/plugin-scaffolder-backend": "1.10.0", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.15", - "@backstage/plugin-scaffolder-backend-module-rails": "0.4.8", - "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.0", - "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.13", - "@backstage/plugin-scaffolder-common": "1.2.4", - "@backstage/plugin-scaffolder-react": "1.0.0", - "@backstage/plugin-search": "1.0.7", - "@backstage/plugin-search-backend": "1.2.1", - "@backstage/plugin-search-backend-module-elasticsearch": "1.1.1", - "@backstage/plugin-search-backend-module-pg": "0.5.1", - "@backstage/plugin-search-backend-node": "1.1.1", + "@backstage/plugin-proxy-backend": "0.2.36", + "@backstage/plugin-rollbar": "0.4.15", + "@backstage/plugin-rollbar-backend": "0.1.39", + "@backstage/plugin-scaffolder": "1.11.0", + "@backstage/plugin-scaffolder-backend": "1.11.0", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.17", + "@backstage/plugin-scaffolder-backend-module-rails": "0.4.10", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.2", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.15", + "@backstage/plugin-scaffolder-common": "1.2.5", + "@backstage/plugin-scaffolder-node": "0.1.0", + "@backstage/plugin-scaffolder-react": "1.1.0", + "@backstage/plugin-search": "1.1.0", + "@backstage/plugin-search-backend": "1.2.3", + "@backstage/plugin-search-backend-module-elasticsearch": "1.1.3", + "@backstage/plugin-search-backend-module-pg": "0.5.3", + "@backstage/plugin-search-backend-node": "1.1.3", "@backstage/plugin-search-common": "1.2.1", - "@backstage/plugin-search-react": "1.4.0", - "@backstage/plugin-sentry": "0.4.7", - "@backstage/plugin-shortcuts": "0.3.6", - "@backstage/plugin-sonarqube": "0.6.2", - "@backstage/plugin-sonarqube-backend": "0.1.5", - "@backstage/plugin-sonarqube-react": "0.1.1", - "@backstage/plugin-splunk-on-call": "0.4.3", - "@backstage/plugin-stack-overflow": "0.1.10", - "@backstage/plugin-stack-overflow-backend": "0.1.9", - "@backstage/plugin-tech-insights": "0.3.6", - "@backstage/plugin-tech-insights-backend": "0.5.6", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.24", - "@backstage/plugin-tech-insights-common": "0.2.9", - "@backstage/plugin-tech-insights-node": "0.3.8", - "@backstage/plugin-tech-radar": "0.6.0", - "@backstage/plugin-techdocs": "1.4.3", - "@backstage/plugin-techdocs-addons-test-utils": "1.0.9", - "@backstage/plugin-techdocs-backend": "1.5.1", - "@backstage/plugin-techdocs-module-addons-contrib": "1.0.9", - "@backstage/plugin-techdocs-node": "1.4.4", - "@backstage/plugin-techdocs-react": "1.1.2", - "@backstage/plugin-todo": "0.2.16", - "@backstage/plugin-todo-backend": "0.1.37", - "@backstage/plugin-user-settings": "0.6.2", - "@backstage/plugin-user-settings-backend": "0.1.4", - "@backstage/plugin-vault": "0.1.8", - "@backstage/plugin-vault-backend": "0.2.6", - "@backstage/plugin-xcmetrics": "0.2.34" + "@backstage/plugin-search-react": "1.5.0", + "@backstage/plugin-sentry": "0.5.0", + "@backstage/plugin-shortcuts": "0.3.7", + "@backstage/plugin-sonarqube": "0.6.3", + "@backstage/plugin-sonarqube-backend": "0.1.7", + "@backstage/plugin-sonarqube-react": "0.1.2", + "@backstage/plugin-splunk-on-call": "0.4.4", + "@backstage/plugin-stack-overflow": "0.1.11", + "@backstage/plugin-stack-overflow-backend": "0.1.11", + "@backstage/plugin-tech-insights": "0.3.7", + "@backstage/plugin-tech-insights-backend": "0.5.8", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.26", + "@backstage/plugin-tech-insights-common": "0.2.10", + "@backstage/plugin-tech-insights-node": "0.4.0", + "@backstage/plugin-tech-radar": "0.6.1", + "@backstage/plugin-techdocs": "1.5.0", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.10", + "@backstage/plugin-techdocs-backend": "1.5.3", + "@backstage/plugin-techdocs-module-addons-contrib": "1.0.10", + "@backstage/plugin-techdocs-node": "1.5.0", + "@backstage/plugin-techdocs-react": "1.1.3", + "@backstage/plugin-todo": "0.2.17", + "@backstage/plugin-todo-backend": "0.1.39", + "@backstage/plugin-user-settings": "0.7.0", + "@backstage/plugin-user-settings-backend": "0.1.6", + "@backstage/plugin-vault": "0.1.9", + "@backstage/plugin-vault-backend": "0.2.8", + "@backstage/plugin-xcmetrics": "0.2.35", + "@backstage/plugin-octopus-deploy": "0.0.0", + "@backstage/plugin-stackstorm": "0.0.0" }, - "changesets": [] + "changesets": [ + "breezy-bees-care", + "bright-kids-raise", + "create-app-1676993958", + "curvy-pets-hang", + "eight-radios-bake", + "eighty-geese-return", + "four-lizards-grin", + "gentle-pears-clean", + "grumpy-bikes-begin", + "happy-boxes-arrive", + "honest-nails-bake", + "long-nails-pump", + "new-jobs-deny", + "nice-planets-wave", + "ninety-turtles-wait", + "odd-fireants-bathe", + "old-foxes-shave", + "perfect-mayflies-greet", + "polite-chicken-do", + "polite-falcons-jump", + "quick-schools-dress", + "quick-tables-listen", + "rare-buses-swim", + "rare-grapes-count", + "rare-pandas-hope", + "silly-suits-run", + "six-melons-rhyme", + "slimy-lobsters-kneel", + "slow-insects-cheat", + "slow-suits-grab", + "spotty-bees-tell", + "spotty-wasps-complain", + "swift-bags-wave", + "ten-tigers-marry", + "thin-candles-wait", + "twenty-jeans-speak", + "yellow-bananas-yawn" + ] } diff --git a/.changeset/purple-days-film.md b/.changeset/purple-days-film.md new file mode 100644 index 0000000000..09d760c33e --- /dev/null +++ b/.changeset/purple-days-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Fixed bug that prevented deletion of stale files when non-empty directories where tried to be deleted as well. diff --git a/.changeset/quick-coats-sneeze.md b/.changeset/quick-coats-sneeze.md new file mode 100644 index 0000000000..24f7f70e83 --- /dev/null +++ b/.changeset/quick-coats-sneeze.md @@ -0,0 +1,5 @@ +--- +'@techdocs/cli': minor +--- + +Running `@techdocs/cli generate` with the `--verbose` flag will now print the mkdocs output. diff --git a/.changeset/quick-schools-dress.md b/.changeset/quick-schools-dress.md new file mode 100644 index 0000000000..511709461e --- /dev/null +++ b/.changeset/quick-schools-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-addons-test-utils': patch +--- + +Fix bug/problem with test helper diff --git a/.changeset/quick-tables-listen.md b/.changeset/quick-tables-listen.md new file mode 100644 index 0000000000..fab9ef7969 --- /dev/null +++ b/.changeset/quick-tables-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stackstorm': minor +--- + +Add StackStorm plugin to Backstage which is interfacing with StackStorm API. It allows users to view workflow executions, packs and actions. For installation and configuration instructions please view [README.md](https://github.com/backstage/backstage/tree/master/plugins/stackstorm/README.md) diff --git a/.changeset/quiet-ghosts-float.md b/.changeset/quiet-ghosts-float.md new file mode 100644 index 0000000000..dc8df90206 --- /dev/null +++ b/.changeset/quiet-ghosts-float.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-entity-validation': patch +'@backstage/core-components': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-playlist': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-explore': patch +--- + +Switch to using `LinkButton` instead of the deprecated `Button` diff --git a/.changeset/rare-buses-swim.md b/.changeset/rare-buses-swim.md new file mode 100644 index 0000000000..4626343ca3 --- /dev/null +++ b/.changeset/rare-buses-swim.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +Added the adapted query to mysql and sqlite3 databases to not returning warning on logs diff --git a/.changeset/rare-grapes-count.md b/.changeset/rare-grapes-count.md new file mode 100644 index 0000000000..fdeb653f3c --- /dev/null +++ b/.changeset/rare-grapes-count.md @@ -0,0 +1,45 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-events-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-events-backend-module-aws-sqs': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-events-backend-module-gerrit': patch +'@backstage/plugin-events-backend-module-github': patch +'@backstage/plugin-events-backend-module-gitlab': patch +'@backstage/plugin-events-backend-module-azure': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/backend-plugin-api': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +'@backstage/core-plugin-api': patch +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-sonarqube-react': patch +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-common': patch +'@backstage/plugin-events-backend': patch +'@backstage/plugin-jenkins-common': patch +'@backstage/plugin-techdocs-react': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-catalog-node': patch +'@backstage/test-utils': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-events-node': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-jenkins': patch +--- + +Internal refactor of `/alpha` exports. diff --git a/.changeset/rare-pandas-hope.md b/.changeset/rare-pandas-hope.md new file mode 100644 index 0000000000..6e8e81fcfb --- /dev/null +++ b/.changeset/rare-pandas-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-playlist': patch +--- + +Internal update to work with newer versions of react-router diff --git a/.changeset/renovate-3672e5e.md b/.changeset/renovate-3672e5e.md new file mode 100644 index 0000000000..c94eec2245 --- /dev/null +++ b/.changeset/renovate-3672e5e.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `del` to `^7.0.0`. diff --git a/.changeset/renovate-4775edd.md b/.changeset/renovate-4775edd.md new file mode 100644 index 0000000000..6472fac254 --- /dev/null +++ b/.changeset/renovate-4775edd.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-graphql': patch +--- + +Updated dependency `@graphql-codegen/cli` to `^3.0.0`. +Updated dependency `@graphql-codegen/graphql-modules-preset` to `^3.0.0`. +Updated dependency `@graphql-codegen/typescript` to `^3.0.0`. +Updated dependency `@graphql-codegen/typescript-resolvers` to `^3.0.0`. diff --git a/.changeset/renovate-488b7e7.md b/.changeset/renovate-488b7e7.md new file mode 100644 index 0000000000..35940f474b --- /dev/null +++ b/.changeset/renovate-488b7e7.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Updated dependency `@types/passport-microsoft` to `^1.0.0`. diff --git a/.changeset/renovate-80f8787.md b/.changeset/renovate-80f8787.md new file mode 100644 index 0000000000..addac46355 --- /dev/null +++ b/.changeset/renovate-80f8787.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-react': patch +'@backstage/plugin-techdocs': patch +--- + +Updated dependency `jss` to `~10.10.0`. diff --git a/.changeset/renovate-fb85ae7.md b/.changeset/renovate-fb85ae7.md new file mode 100644 index 0000000000..920469850c --- /dev/null +++ b/.changeset/renovate-fb85ae7.md @@ -0,0 +1,125 @@ +--- +'@backstage/backend-common': patch +'@backstage/backend-test-utils': patch +'@backstage/catalog-client': patch +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/integration-react': patch +'@backstage/integration': patch +'@backstage/release-manifests': patch +'@backstage/test-utils': patch +'@backstage/plugin-adr-backend': patch +'@backstage/plugin-adr': patch +'@backstage/plugin-airbrake-backend': patch +'@backstage/plugin-airbrake': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-apache-airflow': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-apollo-explorer': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-node': patch +'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-azure-sites-backend': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bitbucket-cloud-common': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-bitbucket': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-graphql': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-code-climate': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-dynatrace': patch +'@backstage/plugin-entity-feedback-backend': patch +'@backstage/plugin-entity-feedback': patch +'@backstage/plugin-entity-validation': patch +'@backstage/plugin-explore-backend': patch +'@backstage/plugin-explore-react': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-gcalendar': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-git-release-manager': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-github-issues': patch +'@backstage/plugin-github-pull-requests-board': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-gocd': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-graphql-backend': patch +'@backstage/plugin-graphql-voyager': patch +'@backstage/plugin-home': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-jenkins-backend': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-linguist-backend': patch +'@backstage/plugin-linguist': patch +'@backstage/plugin-microsoft-calendar': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-octopus-deploy': patch +'@backstage/plugin-org-react': patch +'@backstage/plugin-org': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-periskop-backend': patch +'@backstage/plugin-periskop': patch +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-permission-common': patch +'@backstage/plugin-permission-node': patch +'@backstage/plugin-playlist-backend': patch +'@backstage/plugin-playlist': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-rollbar-backend': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube-backend': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-stack-overflow-backend': patch +'@backstage/plugin-stack-overflow': patch +'@backstage/plugin-stackstorm': patch +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-techdocs-addons-test-utils': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-module-addons-contrib': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-todo-backend': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-vault-backend': patch +'@backstage/plugin-vault': patch +'@backstage/plugin-xcmetrics': patch +--- + +Updated dependency `msw` to `^1.0.0`. diff --git a/.changeset/rich-clocks-approve.md b/.changeset/rich-clocks-approve.md new file mode 100644 index 0000000000..319a8a7c9c --- /dev/null +++ b/.changeset/rich-clocks-approve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-lighthouse-backend': patch +--- + +Update README.md; typo with plugin name & removed unneeded "import { Router } from 'express';" diff --git a/.changeset/rich-wombats-rescue.md b/.changeset/rich-wombats-rescue.md new file mode 100644 index 0000000000..d62f38cb4a --- /dev/null +++ b/.changeset/rich-wombats-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Updated the `aria-label` of the `HeaderTabs` component. diff --git a/.changeset/short-mayflies-fix.md b/.changeset/short-mayflies-fix.md new file mode 100644 index 0000000000..9d68a85da7 --- /dev/null +++ b/.changeset/short-mayflies-fix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-pagerduty': patch +--- + +Update readme link for discord channel diff --git a/.changeset/silly-suits-run.md b/.changeset/silly-suits-run.md new file mode 100644 index 0000000000..ec371e4077 --- /dev/null +++ b/.changeset/silly-suits-run.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Add /entities/by-query endpoint returning paginated entities. + +The endpoint supports cursor base pagination and server side sorting of the entities diff --git a/.changeset/silver-lies-rest.md b/.changeset/silver-lies-rest.md new file mode 100644 index 0000000000..34b77f5375 --- /dev/null +++ b/.changeset/silver-lies-rest.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +The configuration key `branch` of the `GitlabDiscoveryEntityProvider` has been deprecated in favor of the configuration key `fallbackBranch`. +It will be reused in future release to enforce a concrete branch to be used in catalog file discovery. +To migrate to the new configuration value, rename `branch` to `fallbackBranch`. diff --git a/.changeset/six-melons-rhyme.md b/.changeset/six-melons-rhyme.md new file mode 100644 index 0000000000..dbb878fc89 --- /dev/null +++ b/.changeset/six-melons-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +scaffolder/next: Added the ability to get the fields definition in the schema in the validation function diff --git a/.changeset/slimy-lobsters-kneel.md b/.changeset/slimy-lobsters-kneel.md new file mode 100644 index 0000000000..40ff476a64 --- /dev/null +++ b/.changeset/slimy-lobsters-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist-backend': patch +--- + +Fixed bug in LinguistBackendApi that caused initial batch of entities to be skipped. diff --git a/.changeset/slow-insects-cheat.md b/.changeset/slow-insects-cheat.md new file mode 100644 index 0000000000..70b7bbf69b --- /dev/null +++ b/.changeset/slow-insects-cheat.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-techdocs': minor +'@backstage/plugin-catalog': minor +'@backstage/plugin-explore': patch +--- + +Add ability to pass icon as function to have ability to customize it by search item diff --git a/.changeset/slow-pens-compare.md b/.changeset/slow-pens-compare.md new file mode 100644 index 0000000000..158cb6f760 --- /dev/null +++ b/.changeset/slow-pens-compare.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix a bug where the batch fetch by ref endpoint did not work in conjunction with filtering (e.g. if authorization was enabled). diff --git a/.changeset/slow-suits-grab.md b/.changeset/slow-suits-grab.md new file mode 100644 index 0000000000..4a741cac6b --- /dev/null +++ b/.changeset/slow-suits-grab.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +properly support custom graph api URL diff --git a/.changeset/spotty-bees-tell.md b/.changeset/spotty-bees-tell.md new file mode 100644 index 0000000000..5267d8a6a0 --- /dev/null +++ b/.changeset/spotty-bees-tell.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +Fix missing `dependencies` in `package.json` diff --git a/.changeset/spotty-coats-clean.md b/.changeset/spotty-coats-clean.md deleted file mode 100644 index 1a48da5688..0000000000 --- a/.changeset/spotty-coats-clean.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-adr': patch ---- - -Render the common `` component when the `backstage.io/adr-location` annotation is missing from the component diff --git a/.changeset/spotty-turtles-reply.md b/.changeset/spotty-turtles-reply.md new file mode 100644 index 0000000000..2dfc12a176 --- /dev/null +++ b/.changeset/spotty-turtles-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Copy to clipboard now works in a not secure context. diff --git a/.changeset/spotty-wasps-complain.md b/.changeset/spotty-wasps-complain.md new file mode 100644 index 0000000000..7b1e42b2a0 --- /dev/null +++ b/.changeset/spotty-wasps-complain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fix alignment bug for owners on `TemplateCard` diff --git a/.changeset/swift-bags-wave.md b/.changeset/swift-bags-wave.md new file mode 100644 index 0000000000..8f57c7f89b --- /dev/null +++ b/.changeset/swift-bags-wave.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-sonarqube-react': patch +'@backstage/plugin-sonarqube': patch +--- + +Moved imports from `/alpha` to main public exports. diff --git a/.changeset/ten-tigers-marry.md b/.changeset/ten-tigers-marry.md new file mode 100644 index 0000000000..f90f7b3ea2 --- /dev/null +++ b/.changeset/ten-tigers-marry.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-proxy-backend': patch +--- + +The proxy-backend plugin now supports reviving request bodies that have previously been consumed by an express middleware (e.g. `express.json()`). This is done by setting `reviveConsumedRequestBodies: true` on the proxy `RouterOptions`. In order to preserve the current behavior, the proxy will **not** revive request bodies by default. + +The following is an example of a proxy `createRouter` invocation that revives request bodies: + +```diff +const router = await createRouter({ + config, + logger, + discovery, ++ reviveConsumedRequestBodies: true, +}); +``` diff --git a/.changeset/thin-candles-wait.md b/.changeset/thin-candles-wait.md new file mode 100644 index 0000000000..c7e18367b5 --- /dev/null +++ b/.changeset/thin-candles-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sonarqube': patch +--- + +Show a more appropriate icon if there are no code smells and/or vulnerabilities. diff --git a/.changeset/tricky-jars-film.md b/.changeset/tricky-jars-film.md new file mode 100644 index 0000000000..9533634614 --- /dev/null +++ b/.changeset/tricky-jars-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +--- + +Move `@backstage/backend-defaults` to `devDependencies` diff --git a/.changeset/twelve-cars-push.md b/.changeset/twelve-cars-push.md new file mode 100644 index 0000000000..599d456d2b --- /dev/null +++ b/.changeset/twelve-cars-push.md @@ -0,0 +1,20 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-test-utils': patch +'@backstage/backend-dev-utils': patch +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +'@backstage/backend-common': patch +'@backstage/backend-tasks': patch +'@backstage/config-loader': patch +'@backstage/cli-common': patch +'@backstage/create-app': patch +'@backstage/test-utils': patch +'@backstage/dev-utils': patch +'@backstage/codemods': patch +'@backstage/config': patch +'@backstage/theme': patch +'@backstage/cli': patch +--- + +Updated link to docs. diff --git a/.changeset/twenty-jeans-speak.md b/.changeset/twenty-jeans-speak.md new file mode 100644 index 0000000000..fddcc2eedd --- /dev/null +++ b/.changeset/twenty-jeans-speak.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added authentication provider for Azure Easy Authentication. diff --git a/.changeset/unlucky-sloths-explain.md b/.changeset/unlucky-sloths-explain.md new file mode 100644 index 0000000000..3d5f963a73 --- /dev/null +++ b/.changeset/unlucky-sloths-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-module-addons-contrib': patch +--- + +Fixed bug in IssueLink component where the URL was not generated properly when the repository was located inside a Gitlab subgroup diff --git a/.changeset/weak-coats-allow.md b/.changeset/weak-coats-allow.md new file mode 100644 index 0000000000..31fc592326 --- /dev/null +++ b/.changeset/weak-coats-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist': patch +--- + +Updated `README.md` to removed images with refresh button as this feature was not implemented in the current version diff --git a/.changeset/what-is-going-on-babe.md b/.changeset/what-is-going-on-babe.md new file mode 100644 index 0000000000..c22d69d1de --- /dev/null +++ b/.changeset/what-is-going-on-babe.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Add support for `NotImplementedError`, properly returning 501 as status code. diff --git a/.changeset/wicked-lions-repeat.md b/.changeset/wicked-lions-repeat.md new file mode 100644 index 0000000000..c48545c6bc --- /dev/null +++ b/.changeset/wicked-lions-repeat.md @@ -0,0 +1,6 @@ +--- +'@techdocs/cli': minor +'@backstage/plugin-techdocs-node': minor +--- + +Added support for an HTTPS proxy for techdocs AWS S3 requests diff --git a/.changeset/wicked-spoons-call.md b/.changeset/wicked-spoons-call.md new file mode 100644 index 0000000000..c57860349b --- /dev/null +++ b/.changeset/wicked-spoons-call.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-newrelic': patch +--- + +Updated installation instructions diff --git a/.changeset/witty-geckos-design.md b/.changeset/witty-geckos-design.md new file mode 100644 index 0000000000..d482332c23 --- /dev/null +++ b/.changeset/witty-geckos-design.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-linguist-backend': patch +--- + +Allow kind to be configurable + +```ts +return createRouter({ schedule: schedule, kind: ['Component'] }, { ...env }); +``` diff --git a/.changeset/yellow-bananas-yawn.md b/.changeset/yellow-bananas-yawn.md new file mode 100644 index 0000000000..d9c522a429 --- /dev/null +++ b/.changeset/yellow-bananas-yawn.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +The API report generation process is now able to detect and generate reports for additional entry points declared in the package `"exports"` field. diff --git a/.changeset/young-schools-double.md b/.changeset/young-schools-double.md new file mode 100644 index 0000000000..8cba2ec8f3 --- /dev/null +++ b/.changeset/young-schools-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Do not close `Autocomplete` powered multiple-selection filters when checking boxes diff --git a/.changeset/young-scissors-cough.md b/.changeset/young-scissors-cough.md new file mode 100644 index 0000000000..fa35588224 --- /dev/null +++ b/.changeset/young-scissors-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added a workaround for the cyclic dependency bug across `@backstage/backend-common` and `@backstage/backend-app-api`. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 82d868a408..10f1ac188c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,78 +4,80 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @backstage/reviewers -yarn.lock @backstage/reviewers @backstage-service -*/yarn.lock @backstage/reviewers @backstage-service -/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining -/.changeset @backstage/reviewers @backstage/techdocs-core -/cypress/src/integration/plugins/techdocs.spec.ts @backstage/reviewers @backstage/techdocs-core -/docs/assets/search @backstage/reviewers @backstage/techdocs-core -/docs/features/search @backstage/reviewers @backstage/techdocs-core -/docs/features/techdocs @backstage/reviewers @backstage/techdocs-core -/docs/plugins/integrating-search-into-plugins.md @backstage/reviewers @backstage/techdocs-core -/packages/techdocs-cli @backstage/reviewers @backstage/techdocs-core -/packages/techdocs-cli-embedded-app @backstage/reviewers @backstage/techdocs-core -/plugins/adr @backstage/reviewers @kuangp -/plugins/adr-* @backstage/reviewers @kuangp -/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps -/plugins/apache-airflow @backstage/reviewers @cmpadden -/plugins/api-docs @backstage/reviewers @backstage/sda-se-reviewers -/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin -/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin -/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin -/plugins/bitbucket-cloud-common @backstage/reviewers @pjungermann -/plugins/bitrise @backstage/reviewers @backstage/sda-se-reviewers -/plugins/catalog @backstage/reviewers @backstage/catalog-core -/plugins/catalog-* @backstage/reviewers @backstage/catalog-core -/plugins/catalog-backend-module-aws @backstage/reviewers @backstage/catalog-core @pjungermann -/plugins/catalog-backend-module-bitbucket-cloud @backstage/reviewers @backstage/catalog-core @pjungermann -/plugins/catalog-backend-module-msgraph @backstage/reviewers @backstage/catalog-core @pjungermann -/plugins/catalog-graph @backstage/reviewers @backstage/catalog-core @backstage/sda-se-reviewers -/plugins/circleci @backstage/reviewers @adamdmharvey -/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios -/plugins/code-coverage @backstage/reviewers @alde @nissayeva -/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva -/plugins/cost-insights @backstage/reviewers @backstage/silver-lining -/plugins/cost-insights-* @backstage/reviewers @backstage/silver-lining -/plugins/events-backend @backstage/reviewers @pjungermann -/plugins/events-backend-module-aws-sqs @backstage/reviewers @pjungermann -/plugins/events-backend-module-azure @backstage/reviewers @pjungermann -/plugins/events-backend-module-bitbucket-cloud @backstage/reviewers @pjungermann -/plugins/events-backend-module-gerrit @backstage/reviewers @pjungermann -/plugins/events-backend-module-github @backstage/reviewers @pjungermann -/plugins/events-backend-module-gitlab @backstage/reviewers @pjungermann -/plugins/events-backend-test-utils @backstage/reviewers @pjungermann -/plugins/events-node @backstage/reviewers @pjungermann -/plugins/explore @backstage/reviewers @backstage/sda-se-reviewers -/plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers -/plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers -/plugins/gcalendar @backstage/reviewers @szubster @ptychu @kielosz @alexrybch -/plugins/git-release-manager @backstage/reviewers @erikengervall -/plugins/home @backstage/reviewers @backstage/techdocs-core -/plugins/ilert @backstage/reviewers @yacut -/plugins/jenkins @backstage/reviewers @timja -/plugins/jenkins-backend @backstage/reviewers @timja -/plugins/kafka @backstage/reviewers @nirga @andrewthauer -/plugins/kafka-backend @backstage/reviewers @nirga @andrewthauer -/plugins/kubernetes @backstage/reviewers @backstage/warpspeed -/plugins/kubernetes-* @backstage/reviewers @backstage/warpspeed -/plugins/newrelic-dashboard @backstage/reviewers @mufaddal7 -/plugins/playlist @backstage/reviewers @kuangp -/plugins/playlist-* @backstage/reviewers @kuangp -/plugins/rollbar @backstage/reviewers @andrewthauer -/plugins/rollbar-backend @backstage/reviewers @andrewthauer -/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski -/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka -/plugins/search @backstage/reviewers @backstage/techdocs-core -/plugins/search-* @backstage/reviewers @backstage/techdocs-core -/plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers -/plugins/stack-overflow @backstage/reviewers @backstage/techdocs-core -/plugins/stack-overflow-backend @backstage/reviewers @backstage/techdocs-core -/plugins/techdocs @backstage/reviewers @backstage/techdocs-core -/plugins/techdocs-* @backstage/reviewers @backstage/techdocs-core -/plugins/user-settings-backend @backstage/reviewers @backstage/sda-se-reviewers -/tech-insights-backend @backstage/reviewers @xantier @iain-b -/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b -/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b -/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b +* @backstage/maintainers +yarn.lock @backstage/maintainers @backstage-service +*/yarn.lock @backstage/maintainers @backstage-service +/.changeset/*.md +/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core +/docs/assets/search @backstage/techdocs-core +/docs/features/search @backstage/techdocs-core +/docs/features/techdocs @backstage/techdocs-core +/docs/plugins/integrating-search-into-plugins.md @backstage/techdocs-core +/packages/techdocs-cli @backstage/techdocs-core +/packages/techdocs-cli-embedded-app @backstage/techdocs-core +/plugins/adr @backstage/maintainers @kuangp +/plugins/adr-* @backstage/maintainers @kuangp +/plugins/allure @backstage/maintainers @deepak-bhardwaj-ps +/plugins/apache-airflow @backstage/maintainers @cmpadden +/plugins/api-docs @backstage/maintainers @backstage/sda-se-reviewers +/plugins/azure-devops @backstage/maintainers @marleypowell @awanlin +/plugins/azure-devops-backend @backstage/maintainers @marleypowell @awanlin +/plugins/azure-devops-common @backstage/maintainers @marleypowell @awanlin +/plugins/bitbucket-cloud-common @backstage/maintainers @pjungermann +/plugins/bitrise @backstage/maintainers @backstage/sda-se-reviewers +/plugins/catalog @backstage/maintainers @backstage/catalog-core +/plugins/catalog-* @backstage/maintainers @backstage/catalog-core +/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/catalog-core @pjungermann +/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/catalog-core @pjungermann +/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-core @pjungermann +/plugins/catalog-graph @backstage/maintainers @backstage/catalog-core @backstage/sda-se-reviewers +/plugins/circleci @backstage/maintainers @adamdmharvey +/plugins/cloudbuild @backstage/maintainers @trivago/ebarrios +/plugins/code-coverage @backstage/maintainers @alde @nissayeva +/plugins/code-coverage-backend @backstage/maintainers @alde @nissayeva +/plugins/cost-insights @backstage/maintainers @backstage/silver-lining +/plugins/cost-insights-* @backstage/maintainers @backstage/silver-lining +/plugins/entity-feedback @backstage/maintainers @kuangp +/plugins/entity-feedback-* @backstage/maintainers @kuangp +/plugins/events-backend @backstage/maintainers @pjungermann +/plugins/events-backend-module-aws-sqs @backstage/maintainers @pjungermann +/plugins/events-backend-module-azure @backstage/maintainers @pjungermann +/plugins/events-backend-module-bitbucket-cloud @backstage/maintainers @pjungermann +/plugins/events-backend-module-gerrit @backstage/maintainers @pjungermann +/plugins/events-backend-module-github @backstage/maintainers @pjungermann +/plugins/events-backend-module-gitlab @backstage/maintainers @pjungermann +/plugins/events-backend-test-utils @backstage/maintainers @pjungermann +/plugins/events-node @backstage/maintainers @pjungermann +/plugins/explore @backstage/maintainers @backstage/sda-se-reviewers +/plugins/explore-react @backstage/maintainers @backstage/sda-se-reviewers +/plugins/fossa @backstage/maintainers @backstage/sda-se-reviewers +/plugins/gcalendar @backstage/maintainers @szubster @ptychu @kielosz @alexrybch +/plugins/git-release-manager @backstage/maintainers @erikengervall +/plugins/home @backstage/techdocs-core +/plugins/ilert @backstage/maintainers @yacut +/plugins/jenkins @backstage/maintainers @timja +/plugins/jenkins-backend @backstage/maintainers @timja +/plugins/kafka @backstage/maintainers @nirga @andrewthauer +/plugins/kafka-backend @backstage/maintainers @nirga @andrewthauer +/plugins/kubernetes @backstage/maintainers @backstage/warpspeed +/plugins/kubernetes-* @backstage/maintainers @backstage/warpspeed +/plugins/microsoft-calendar @backstage/maintainers @abhay-soni-developer +/plugins/newrelic-dashboard @backstage/maintainers @mufaddal7 +/plugins/playlist @backstage/maintainers @kuangp +/plugins/playlist-* @backstage/maintainers @kuangp +/plugins/rollbar @backstage/maintainers @andrewthauer +/plugins/rollbar-backend @backstage/maintainers @andrewthauer +/plugins/scaffolder-backend-module-rails @backstage/maintainers @angeliski +/plugins/scaffolder-backend-module-yeoman @backstage/maintainers @pawelmitka +/plugins/search @backstage/techdocs-core +/plugins/search-* @backstage/techdocs-core +/plugins/sonarqube @backstage/maintainers @backstage/sda-se-reviewers +/plugins/stack-overflow @backstage/techdocs-core +/plugins/stack-overflow-backend @backstage/techdocs-core +/plugins/techdocs @backstage/techdocs-core +/plugins/techdocs-* @backstage/techdocs-core +/plugins/user-settings-backend @backstage/maintainers @backstage/sda-se-reviewers +/tech-insights-backend @backstage/maintainers @xantier @iain-b +/tech-insights-backend-module-jsonfc @backstage/maintainers @xantier @iain-b +/tech-insights-tech-insights-common @backstage/maintainers @xantier @iain-b +/tech-insights-tech-insights-node @backstage/maintainers @xantier @iain-b diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 782b5fdfa0..57595621e4 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -1,6 +1,6 @@ name: '🐛 Bug Report' description: 'Submit a bug report to help us improve' -title: '🐛 Bug Report: ' +title: '🐛 Bug Report: ' labels: - bug body: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e9f23287b7..e4b99b3be1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,7 +6,7 @@ contact_links: url: 'https://github.com/backstage/backstage/discussions' - about: 'Alternatively, you can use the Backstage Community Discord' name: Chat - url: 'https://discord.gg/MUpMjP2' + url: 'https://discord.gg/backstage-687207715902193673' - about: 'Please check the FAQ before filing new issues' name: 'Backstage FAQ' url: 'https://backstage.io/docs/FAQ' diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index b5e06d48cf..b2d5a6be81 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -1,6 +1,6 @@ name: 🚀 Feature description: 'Submit a proposal for a new feature' -title: '🚀 Feature: ' +title: '🚀 Feature: <title>' labels: [enhancement] body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/plugin.yaml b/.github/ISSUE_TEMPLATE/plugin.yaml index a36c427981..a7f5216631 100644 --- a/.github/ISSUE_TEMPLATE/plugin.yaml +++ b/.github/ISSUE_TEMPLATE/plugin.yaml @@ -1,6 +1,6 @@ name: 🔌 Plugin description: 'Submit a proposal for a new Plugin' -title: '🔌 Plugin: ' +title: '🔌 Plugin: <title>' labels: [plugin] body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/rfc.yaml b/.github/ISSUE_TEMPLATE/rfc.yaml index 775c64e674..8e73992dc8 100644 --- a/.github/ISSUE_TEMPLATE/rfc.yaml +++ b/.github/ISSUE_TEMPLATE/rfc.yaml @@ -1,6 +1,6 @@ name: 💬 RFC description: 'Request For Comments (RFC) from the community' -title: '💬 RFC: ' +title: '💬 RFC: <title>' labels: [rfc] body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/ux_component.yaml b/.github/ISSUE_TEMPLATE/ux_component.yaml index 453f1e8278..c528356eb3 100644 --- a/.github/ISSUE_TEMPLATE/ux_component.yaml +++ b/.github/ISSUE_TEMPLATE/ux_component.yaml @@ -1,6 +1,6 @@ name: 🦄 UX Component description: 'For designers to request UX components to be added to the Backstage Storybook' -title: '🦄 UX Component: ' +title: '🦄 UX Component: <title>' labels: [design] body: - type: markdown diff --git a/.github/uffizzi/docker-compose.uffizzi.yml b/.github/uffizzi/docker-compose.uffizzi.yml index 49039cad2a..76a7b4eafc 100644 --- a/.github/uffizzi/docker-compose.uffizzi.yml +++ b/.github/uffizzi/docker-compose.uffizzi.yml @@ -10,9 +10,10 @@ services: image: '${BACKSTAGE_IMAGE}' environment: - POSTGRES_HOST: db + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 POSTGRES_USER: postgres - POSTGRES_PASSWORD: example + POSTGRES_PASSWORD: kiTMoTsiEuyQ43GrL4Hv NODE_ENV: production deploy: resources: @@ -26,4 +27,8 @@ services: db: image: postgres environment: - POSTGRES_PASSWORD: example + POSTGRES_PASSWORD: kiTMoTsiEuyQ43GrL4Hv + deploy: + resources: + limits: + memory: 250M diff --git a/.github/uffizzi/uffizzi.production.app-config.yaml b/.github/uffizzi/uffizzi.production.app-config.yaml index d4b70641da..30cffa11c9 100644 --- a/.github/uffizzi/uffizzi.production.app-config.yaml +++ b/.github/uffizzi/uffizzi.production.app-config.yaml @@ -9,13 +9,17 @@ backend: baseUrl: ${UFFIZZI_URL} auth: keys: - # random mock key for uffizi deployments + # random mock key for Uffizzi deployments - secret: 5TXvdjVZFxF7qf9K5RAYRDoGrLzJooqa listen: port: 7007 database: - client: better-sqlite3 - connection: ':memory:' + client: pg + connection: + host: ${POSTGRES_HOST} + port: ${POSTGRES_PORT} + user: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} cache: store: memory cors: diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 98160a496d..63fa474115 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -9,6 +9,7 @@ Airbrake Airbrakes Alaria Alef +allowlisted Anddddd Apdex api @@ -23,6 +24,7 @@ Avro backend's backported backporting +BEPs Bigtable Billett bitbucket @@ -31,6 +33,7 @@ Blackbox bool boolean Brex +bugfixes builtins callout CDNs @@ -70,6 +73,7 @@ cron cronjobs crontab css +daemonsets Datadog dataflow dayjs @@ -94,6 +98,7 @@ dockerfiles Dockerize dockerode Docusaurus +Dominik DOMPurify don'ts dynatrace @@ -118,6 +123,7 @@ Firekube Firestore Fiverr Francesco +Frontside gerrit Gerrit gitbeaker @@ -137,6 +143,7 @@ Hackathons haproxy hardcoded Helidon +Henneke Heroku hoc horizontalpodautoscalers @@ -166,17 +173,20 @@ jsonnet jsx JWTs Kaewkasi +Kaswell Keyv Knex KPIs +Kuang kubectl kubernetes -kubernetes +Lainfiesta ldap learnings Leasot lerna Lerna +limitranges LocalStack lockdown lockfile @@ -231,6 +241,7 @@ OAuth octokit oidc Okta +Olausson Oldsberg onboarding Onboarding @@ -334,6 +345,7 @@ subfolders subheader subheaders subkey +subpath subroutes subtree superfences @@ -410,8 +422,3 @@ zod Zolotusky zoomable zsh -Lainfiesta -allowlisted -Dominik -Henneke -Kuang diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index e18aa9d428..f96a9fbb39 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -1,6 +1,7 @@ name: Automate changeset feedback on: pull_request_target: + branches: ['master'] permissions: pull-requests: write @@ -28,13 +29,13 @@ jobs: ref: 'refs/pull/${{ github.event.pull_request.number }}/merge' - name: fetch base run: git fetch --depth 1 origin ${{ github.base_ref }} - - uses: backstage/actions/changeset-feedback@v0.5.13 + - uses: backstage/actions/changeset-feedback@v0.6.2 name: Generate feedback with: - diffRef: 'origin/master' + diff-ref: 'origin/master' marker: <!-- changeset-feedback --> issue-number: ${{ github.event.pull_request.number }} - botUsername: backstage-goalie[bot] + bot-username: backstage-goalie[bot] app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10db591f4c..18af36c8c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -63,7 +63,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -181,7 +181,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index dc221c1a1e..be27a732d6 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -8,7 +8,7 @@ jobs: cron: runs-on: ubuntu-latest steps: - - uses: backstage/actions/cron@v0.5.13 + - uses: backstage/actions/cron@v0.6.2 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index d8470d4124..60affcb328 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -26,7 +26,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -50,7 +50,7 @@ jobs: uses: docker/setup-buildx-action@v2 - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: './example-app' file: ./example-app/packages/backend/Dockerfile diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index 1f87c30af2..ec7124ed35 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -14,7 +14,8 @@ jobs: env: CI: true - NODE_OPTIONS: --max-old-space-size=4096 + NODE_OPTIONS: --max-old-space-size=7168 + DOCUSAURUS_SSR_CONCURRENCY: 5 steps: - uses: actions/checkout@v3 @@ -32,7 +33,7 @@ jobs: run: yarn install --immutable - name: microsite yarn install run: yarn install --immutable - working-directory: microsite + working-directory: microsite-next - name: storybook yarn install run: yarn install --immutable working-directory: storybook @@ -42,20 +43,20 @@ jobs: - name: build microsite run: yarn build - working-directory: microsite + working-directory: microsite-next - name: build storybook run: yarn build-storybook working-directory: storybook - name: move storybook dist into microsite - run: mv storybook/dist/ microsite/build/backstage/storybook + run: mv storybook/dist/ microsite-next/build/storybook - name: Check the build output - run: ls microsite/build/backstage && ls microsite/build/backstage/storybook + run: ls microsite-next/build && ls microsite-next/build/storybook - name: Deploy both microsite and storybook to gh-pages uses: JamesIves/github-pages-deploy-action@v4.4.1 with: branch: gh-pages - folder: microsite/build/backstage + folder: microsite-next/build diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index 5f337ace3e..49907042b5 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -26,7 +26,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index c05b850883..058f4f6089 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -68,7 +68,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -145,7 +145,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/issue.yaml b/.github/workflows/issue.yaml index d16d0b55d7..e64f919bdb 100644 --- a/.github/workflows/issue.yaml +++ b/.github/workflows/issue.yaml @@ -10,4 +10,4 @@ jobs: if: github.repository == 'backstage/backstage' steps: - name: Issue sync - uses: backstage/actions/issue-sync@v0.5.13 + uses: backstage/actions/issue-sync@v0.6.2 diff --git a/.github/workflows/pr-review-comment.yaml b/.github/workflows/pr-review-comment.yaml index 11b590809a..52db29c699 100644 --- a/.github/workflows/pr-review-comment.yaml +++ b/.github/workflows/pr-review-comment.yaml @@ -35,7 +35,7 @@ jobs: const prNumber = artifact.name.slice('pr_number-'.length) core.setOutput('pr-number', prNumber); - - uses: backstage/actions/re-review@v0.5.13 + - uses: backstage/actions/re-review@v0.6.2 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index acbd11aece..db7d141abd 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -18,12 +18,12 @@ jobs: if: github.repository == 'backstage/backstage' && ( github.event.pull_request || github.event.issue.pull_request ) steps: - name: PR sync - uses: backstage/actions/pr-sync@v0.5.13 + uses: backstage/actions/pr-sync@v0.6.2 with: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }} project-id: PVT_kwDOBFKqdc02LQ - excluded-users: ${{ secrets.OOO_USERS }} + auto-assign: false owning-teams: '@backstage/techdocs-core' diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 19cc5f8045..b299f314a7 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -20,12 +20,12 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: Run Prettier on ADOPTERS.md - uses: creyD/prettier_action@v4.2 + uses: creyD/prettier_action@v4.3 with: # Modifies commit only if prettier autofixed the ADOPTERS.md prettier_options: --config docs/prettier.config.js --write ADOPTERS.md diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index c6d00ecd28..8cf6991aaf 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -53,10 +53,6 @@ jobs: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} # TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while script: | - const releaseVersion = require('./backstage/package.json').version; - if(releaseVersion.includes('next')) { - return; - } console.log('Dispatching upgrade helper sync'); await github.rest.actions.createWorkflowDispatch({ owner: 'backstage', @@ -65,6 +61,6 @@ jobs: ref: 'master', inputs: { version: require('./backstage/packages/create-app/package.json').version, - releaseVersion + releaseVersion: require('./backstage/package.json').version }, }); diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 9dcc9f63ef..a89b7c8e65 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -23,7 +23,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml index a23216a9cb..d8776d74c7 100644 --- a/.github/workflows/uffizzi-build.yml +++ b/.github/workflows/uffizzi-build.yml @@ -27,7 +27,7 @@ jobs: registry-url: https://registry.npmjs.org/ - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: linux-v16 @@ -58,7 +58,7 @@ jobs: tags: type=raw,value=60d - name: Build Image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: packages/backend/Dockerfile @@ -89,16 +89,11 @@ jobs: name: preview-spec path: docker-compose.rendered.yml retention-days: 2 - - name: Serialize PR Event to File - run: | - cat << EOF > event.json - ${{ toJSON(github.event) }} - EOF - name: Upload PR Event as Artifact uses: actions/upload-artifact@v3 with: name: preview-spec - path: event.json + path: ${{ github.event_path }} retention-days: 2 delete-preview: @@ -107,14 +102,9 @@ jobs: if: ${{ github.event.action == 'closed' }} steps: # If this PR is closing, we will not render a compose file nor pass it to the next workflow. - - name: Serialize PR Event to File - run: | - cat << EOF > event.json - ${{ toJSON(github.event) }} - EOF - name: Upload PR Event as Artifact uses: actions/upload-artifact@v3 with: name: preview-spec - path: event.json + path: ${{ github.event_path }} retention-days: 2 diff --git a/.github/workflows/uffizzi-preview.yaml b/.github/workflows/uffizzi-preview.yaml index 3c344f5a78..6390671bbd 100644 --- a/.github/workflows/uffizzi-preview.yaml +++ b/.github/workflows/uffizzi-preview.yaml @@ -11,6 +11,7 @@ jobs: cache-compose-file: name: Cache Compose File runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} outputs: compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }} pr-number: ${{ env.PR_NUMBER }} @@ -47,7 +48,7 @@ jobs: run: | echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV cat event.json >> $GITHUB_ENV - echo 'EOF' >> $GITHUB_ENV + echo -e '\nEOF' >> $GITHUB_ENV - name: Hash Rendered Compose File id: hash @@ -77,6 +78,7 @@ jobs: name: Use Remote Workflow to Preview on Uffizzi needs: - cache-compose-file + if: ${{ github.event.workflow_run.conclusion == 'success' }} uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2 with: # If this workflow was triggered by a PR close event, cache-key will be an empty string diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index f687e4f5a4..4fda0749c0 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -52,7 +52,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 8a49b8fe31..145eefbd26 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -49,7 +49,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.1.3 + uses: microsoft/setup-msbuild@v1.3.1 - name: setup chrome uses: browser-actions/setup-chrome@latest - name: yarn install diff --git a/.github/workflows/verify_kubernetes.yml b/.github/workflows/verify_kubernetes.yml index 7fbcf9fa3f..83280f8e85 100644 --- a/.github/workflows/verify_kubernetes.yml +++ b/.github/workflows/verify_kubernetes.yml @@ -28,7 +28,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_microsite-next.yml b/.github/workflows/verify_microsite-next.yml index ea0cbae0be..6c9edf2031 100644 --- a/.github/workflows/verify_microsite-next.yml +++ b/.github/workflows/verify_microsite-next.yml @@ -17,6 +17,7 @@ jobs: env: CI: true NODE_OPTIONS: --max-old-space-size=4096 + DOCUSAURUS_SSR_CONCURRENCY: 5 steps: - uses: actions/checkout@v3 @@ -28,10 +29,16 @@ jobs: # Skip caching of microsite dependencies, it keeps the global cache size # smaller, which make Windows builds a lot faster for the rest of the project. + - name: top-level install + run: yarn install --immutable + - name: yarn install run: yarn install --immutable working-directory: microsite-next + - name: build API reference + run: yarn build:api-docs + - name: verify yarn dependency duplicates run: node scripts/verify-lockfile-duplicates.js diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 0e63393af8..8cde5b6e9b 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -35,7 +35,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: storybook yarn install diff --git a/.prettierignore b/.prettierignore index 8f1fae0456..ad2bd355ed 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,7 @@ coverage *.hbs templates api-report.md +*-api-report.md cli-report.md plugins/scaffolder-backend/sample-templates .vscode diff --git a/ADOPTERS.md b/ADOPTERS.md index 7ed897f351..068928260d 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -9,7 +9,6 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [bol.com](https://www.bol.com) | [@acierto](https://github.com/acierto), [@clanghout](https://github.com/clanghout) | Initial work being done to unify platform tooling. | | [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | | [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | | [SDA SE](https://sda.se) | [@dschwank](https://github.com/dschwank), [@iammnils](https://github.com/iammnils) | Central place for developing and sharing services in our insurance ecosystem. | | [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | | [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | @@ -159,7 +158,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Doctolib](https://doctolib.engineering/) | [@djiit](https://github.com/djiit) | Rails modularization effort awareness, tech organization discoverability. Improving the daily workflows and collaboration processes of our engineers. | | [Twilio](https://www.twilio.com) | [Kyle Smith](https://github.com/knksmith57) | Developer portal, universal software catalog, and centralized taxonomy platform. | | [OVHcloud](https://www.ovhcloud.com/fr/) | [Jean-Philippe Blary](https://github.com/blaryjp), [Arnaud Bauer](mailto:arnaud.bauer@ovhcloud.com), [Flavien Chantelot](https://github.com/Dorn-) | We're providing Backstage to our collaborators to ease their daily jobs, and let them extends it using plugins. | -| [Procter & Gamble](https://us.pg.com/) | [Binita Nayak](https://github.com/binitan), [Josh Rose](https://github.com/joshuarose), [RJ Winkler](https://github.com/rjwink) | P&G leverages Backstage to build internal developer portal to ensure developers' happiness. This developer portal shall act as single source of information needed by development teams to seamlessly create, find and maintain their software components/resources/documentation. | +| [Procter & Gamble](https://us.pg.com/) | [Binita Nayak](https://github.com/binitan), | P&G leverages Backstage to build internal developer portal to ensure developers' happiness. This developer portal shall act as single source of information needed by development teams to seamlessly create, find and maintain their software components/resources/documentation. | | [SANS Institute](https://www.sans.org) | [Christopher Klewin](mailto:cklewin@sans.org) | Developer portal for centralized visibility, reporting, and tooling across multiple organizations. | | [Okay](https://www.okayhq.com/) | [Tomas Barreto](mailto:tomas@okayhq.com) | Service catalog, developer portal, and technical documentation | | [Kaluza](https://www.kaluza.com) | [James Condron](mailto:james.condron@kaluza.com) | To provide an automated golden path to developers, with a focus on discovery and documentation | @@ -186,7 +185,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Cho Tot](https://www.chotot.com) | [Chotot Team](mailto:sre@chotot.vn) | Internal developer portal, service catalog with CI/CD tools. | | [William Hill](https://www.williamhillgroup.com/) | [Pat Mills](mailto:pat.mills@williamhill.com), [Nathan Flynn](mailto:nflynn@williamhill.co.uk), and [Nishkarsh Raj](mailto:nishkarsh.raj@williamhill.co.uk) | William Hill are leveraging Backstage to build our Engineering Portal. Our mission is to centralize the software catalog inventory to enable service discoverability, reduce the onboarding time for new Engineers, provide a single pane of glass to accelerate Developer Productivity and Save Engineers time. Our aspiration is to create an InnerSource community focussed on organization-wide patterns that are re-usable and can be self-served with the Scaffolder. | | [Vodafone NewZealand Limited](https://vodafone.co.nz) | [Ankit Gupta](mailto:ankit.gupta@vodafone.nz), [DevOps COE](mailto:devopstooling@vodafone.nz) | Vodafone NZ are leveraging Backstage to build centralised and self service Engineering Portal. Our mission is to standardised Pipeline templates across the Engineering teams, One shop stop to create the pipelines and repository with a template approach which reduces creation part from days to minutes and no wait time for developers. A unified view for Azure DevOps pipeline, Azure Repo pull requests, Deployment status from Azure RedHat Openshift-ArgoCD and SonarQube Security and code quality scans report on a single pan to provide a streamlined view for all microservices across the app stack. | -| [Coamo](http://www.coamo.com.br) | [@holiiveira](https://github.com/holiiveira), [@gpxlnx](https://github.com/gpxlnx) | We're starting to use it as the main tool of a DevOps platform. Our goal is to provide software templates, centralize our software catalog enabling efficient service discovery, and make it easy to manage the entire software ecosystem in one place. | +| [Coamo](http://www.coamo.com.br) | [Henrique Oliveira](https://www.linkedin.com/in/oliveirah/), [Guilherme Xavier](https://br.linkedin.com/in/guilhermexavierlnx) | We're starting to use it as the main tool of a DevOps platform. Our goal is to provide software templates, centralize our software catalog enabling efficient service discovery, and make it easy to manage the entire software ecosystem in one place. | | [Exclaimer](https://www.exclaimer.com/) | [Marley Powell](mailto:marley.powell@exclaimer.com) | Developer portal, service catalog and documentation | | [Edge Consulting](https://egde.no/) | [Øyvind Brekkhus Sandåker](mailto:oyvindbs@egde.no) | Using it as a Software Catalog to get an overview of our systems, for ourselves and our customers. | | [CRV](https://crv4all.com) | [Pepijn Schildkamp](mailto:pepijn.schildkamp@crv4all.com) | @@ -228,3 +227,6 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Booking.com](https://www.linkedin.com/company/booking.com/) | [Mesut Yilmazyildirim](https://www.linkedin.com/in/myilmazyildirim) | We are adopting Backstage as the new reliability platform inside the company. We are migrating UIs of our internal developer tools to Backstage for a better user experience. | [Swissquote Bank](https://swissquote.com/company/jobs/open-positions) | [Bruno Rocha](https://www.linkedin.com/in/bruno-rocha1/) | Integrating Backstage as the visualization layer & tactical overview of our services and teams. | [XP Inc.](https://www.linkedin.com/company/xpinc/) | [Gabriel Santos](https://www.linkedin.com/in/gabriel-santos-6bb740a/) | Developer Portal Catalog (components, APIs, resources, users, groups) and organization relations. +| [OVO Energy](https://www.ovoenergy.com/) | [Michael Wizner](https://github.com/mwz), [Dan Laird](https://github.com/dlaird-ovo), [Samantha Betts](https://github.com/sammbetts) | Developer Experience Tool with an aim to to improve processes, boost productivity, finding of information/docs and building tech engagement throughout the business. +| [MusicTribe](https://careers.musictribe.com/) | [Alex Ford](mailto:alex.j.ford@gmail.com), [Tiago Barbosa](https://github.com/t1agob) | We are starting to use Backstage as a developer portal to share API specifications and documentation, to quickly onboard new projects with the software templates, and to help developers discover software through the catalog. +| [Cazoo](https://www.cazoo.co.uk/) | [Abz Mungul](https://www.linkedin.com/in/abzmungul/), [Scott Edwards](https://www.linkedin.com/in/scott-edwards-tech/) | We're assessing Backstage as our developer platform at Cazoo with a focus on reducing cognitive load for our engineers. We're currently aiming for 3 outcomes: creating visibility into service ownership across teams, improving the discoverability of event schemas and relationships, and improving the discoverability of technical documentation and best practices. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3de7bf0e5..c2df005a1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) So...feel ready to jump in? Let's do this. 👏🏻💯 -Start by reading our [Getting Started for Contributors](https://backstage.io/docs/getting-started/contributors) page to get yourself setup with a fresh copy of Backstage ready for your contributions. If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2). +Start by reading our [Getting Started for Contributors](https://backstage.io/docs/getting-started/contributors) page to get yourself setup with a fresh copy of Backstage ready for your contributions. If you need help, just jump into our [Discord chatroom](https://discord.gg/backstage-687207715902193673). ## Coding Guidelines @@ -136,7 +136,7 @@ Changesets **are** needed for new packages, as that is what triggers the package 1. Run `yarn changeset` 2. Select which packages you want to include a changeset for -3. Select impact of change that you're introducing, using `minor` for breaking changes and `patch` otherwise. We do not use `major` changes while packages are at version `0.x`. +3. Select impact of the change you're introducing. If the package you are changing is at version `0.x`, use `minor` for breaking changes and `patch` otherwise. If the package is at `1.0.0` or higher, use `major` for breaking changes, `minor` for backwards compatible API changes, and `patch` otherwise. See the [Semantic Versioning specification](https://semver.org/#semantic-versioning-specification-semver) for more details. 4. Explain your changes in the generated changeset. See [examples of well written changesets](https://backstage.io/docs/getting-started/contributors#writing-changesets). 5. Add generated changeset to Git 6. Push the commit with your changeset to the branch associated with your PR diff --git a/README.md b/README.md index 8a2bd338d9..60324a7789 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CNCF Status](https://img.shields.io/badge/cncf%20status-incubation-blue.svg)](https://www.cncf.io/projects) [![Main CI Build](https://github.com/backstage/backstage/workflows/Main%20Master%20Build/badge.svg)](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22) -[![Discord](https://img.shields.io/discord/687207715902193673)](https://discord.gg/EBHEGzX) +[![Discord](https://img.shields.io/discord/687207715902193673)](https://discord.gg/backstage-687207715902193673) ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) [![Codecov](https://img.shields.io/codecov/c/github/backstage/backstage)](https://codecov.io/gh/backstage/backstage) [![](https://img.shields.io/github/v/release/backstage/backstage)](https://github.com/backstage/backstage/releases) @@ -45,7 +45,7 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how ## Community -- [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the project +- [Discord chatroom](https://discord.gg/backstage-687207715902193673) - Get support or discuss the project - [Contributing to Backstage](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) - Start here if you want to contribute - [RFCs](https://github.com/backstage/backstage/labels/rfc) - Help shape the technical direction - [FAQ](https://backstage.io/docs/FAQ) - Frequently Asked Questions diff --git a/STYLE.md b/STYLE.md index 87e0644759..e38354ae1d 100644 --- a/STYLE.md +++ b/STYLE.md @@ -94,6 +94,13 @@ This section describes guidelines for designing public APIs. It can also be appl /* ... */ } + // In order to make a private constructor available for testing you can use a + // static factory marked as `@internal`, which will not show up in the public API. + /** @internal */ + static forTesting(internalOptions?: { ... }) { + return new DefaultImageLoader(internalOptions); + } + private constructor(/* ... */) { /* ... */ } diff --git a/app-config.yaml b/app-config.yaml index dd72051189..22f3f4c9c5 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -17,7 +17,7 @@ app: - title: Discord Chatroom icon: chat links: - - url: https://discord.gg/MUpMjP2 + - url: https://discord.gg/backstage-687207715902193673 title: '#backstage' backend: @@ -118,6 +118,11 @@ proxy: headers: Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}' + '/stackstorm': + target: https://your.stackstorm.instance.com/api + headers: + St2-Api-Key: ${ST2_API_KEY} + organization: name: My Company @@ -441,5 +446,8 @@ apacheAirflow: gocd: baseUrl: https://your.gocd.instance.com +stackstorm: + webUrl: https://your.stackstorm.webui.instance.com + permission: enabled: true diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js index a77de4a629..07f264452d 100644 --- a/cypress/.eslintrc.js +++ b/cypress/.eslintrc.js @@ -2,15 +2,6 @@ module.exports = { extends: [require.resolve('@backstage/cli/config/eslint.backend')], rules: { 'no-console': 0, - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: false, - peerDependencies: false, - bundledDependencies: false, - }, - ], 'jest/valid-expect': 'off', 'jest/expect-expect': 'off', 'no-restricted-syntax': 'off', diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 0000000000..fd4db77d61 --- /dev/null +++ b/cypress/support/commands.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. + */ + +// *********************************************** +// This example commands.ts 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) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable<void> +// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element> +// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element> +// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element> +// } +// } +// } diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 0000000000..c71790471e --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This example support/e2e.ts 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'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/cypress/yarn.lock b/cypress/yarn.lock index ded043cdaf..36dd0b056d 100644 --- a/cypress/yarn.lock +++ b/cypress/yarn.lock @@ -1415,22 +1415,22 @@ __metadata: linkType: hard "typescript@npm:^4.1.3": - version: 4.9.4 - resolution: "typescript@npm:4.9.4" + version: 4.9.5 + resolution: "typescript@npm:4.9.5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: e782fb9e0031cb258a80000f6c13530288c6d63f1177ed43f770533fdc15740d271554cdae86701c1dd2c83b082cea808b07e97fd68b38a172a83dbf9e0d0ef9 + checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db languageName: node linkType: hard "typescript@patch:typescript@^4.1.3#~builtin<compat/typescript>": - version: 4.9.4 - resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=a1c5e5" + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=a1c5e5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 37f6e2c3c5e2aa5934b85b0fddbf32eeac8b1bacf3a5b51d01946936d03f5377fe86255d4e5a4ae628fd0cd553386355ad362c57f13b4635064400f3e8e05b9d + checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 languageName: node linkType: hard diff --git a/docs/FAQ.md b/docs/FAQ.md index fcd5542f74..5b93f07d78 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -231,7 +231,7 @@ We take security seriously. When it comes to packages and code we scan our repositories periodically and update our packages to the latest versions. When it comes to deployment of Backstage within an organisation it depends on the deployment and security setup in your organisation. Reach out to us on -[Discord](https://discord.gg/MUpMjP2) if you have specific queries. +[Discord](https://discord.gg/backstage-687207715902193673) if you have specific queries. Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub. diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index c63a12b589..0000000000 --- a/docs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Documentation - -The Backstage documentation is available at https://backstage.io/docs diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index a69c874307..44c00d1ab7 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -290,9 +290,7 @@ The figure below shows the relationship between <span class="textPurple" className="textPurple"> FooApi</span> via the <span class="textRed" className="textRed"> fooApiRef</span>. -<div className="textAlignCenter" class="textAlignCenter"> -<img src="../assets/utility-apis-fig1.svg" alt="Figure showing the relationship between utility APIs, the apps that provide them, and the plugins that consume them" /> -</div> +![Figure showing the relationship between utility APIs, the apps that provide them, and the plugins that consume them](../assets/utility-apis-fig1.svg) The current method for connecting Utility API providers and consumers is via the React tree using an `ApiProvider`, which is added to the `AppProvider` of the diff --git a/docs/assets/auth/gcp-iap-jwt-audience-code-popup.png b/docs/assets/auth/gcp-iap-jwt-audience-code-popup.png new file mode 100644 index 0000000000..72b69b3b01 Binary files /dev/null and b/docs/assets/auth/gcp-iap-jwt-audience-code-popup.png differ diff --git a/docs/assets/backend-system/architecture-building-blocks.drawio.svg b/docs/assets/backend-system/architecture-building-blocks.drawio.svg new file mode 100644 index 0000000000..7a65f3a725 --- /dev/null +++ b/docs/assets/backend-system/architecture-building-blocks.drawio.svg @@ -0,0 +1,224 @@ +<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="642px" height="327px" viewBox="-0.5 -0.5 642 327" content="<mxfile scale="1.5" border="20"><diagram id="gKBvn7eBFHudv9VMIRD-" name="Page-1">3VnbTuMwEP2aPFLl1tsjlMLuw0pI3dXCo0ncxMK1K8dtU75+7dhJnBhKFMJlGySUGV/nnDNjE5xgsclvGdimv2gMseO7ce4E147vz1xX/JaOo3JMZ9qRMBQrl1c7VugZamfZbYdimDU6ckoxR9umM6KEwIg3fIAxemh2W1PcXHULEmg5VhHAtvcvinmqvEHo1v4fECVpuXIZ3iOInhJGd0Qv5/jBunhU8waUU+n+WQpiejBcwdIJFoxSrt42+QJiiWyJmhp380prtW0GCe8ywFcD9gDvdORXIgJI4kzvjx9LSIqooBznOsHVIUUcrrYgkq0HoQHhS/kGC8sTr2uE8YJiyoqxwXIif4Q/44w+QaNlUjxyBCXc8KtH+O2IdJB7yDjMDZeO8BbSDeTsKLro1otAo63FeFGif6ipDUvFpgatJd1Aqymppq4hFS8a1ZcRDiyEV5DtUQTPCWGvhfDUBnj2QfjOLXzvGN2L6mHBC2OR4toklMAmnjBH/N54f5A0jMbaus41K4VxLA0SX8piI8xHTKOn3ykiyn2DcDUxKetbWNADGDfsJueSIt04P8VMRncs0mGFujwClkDda6xcMuCT7DGIAUf7Zs17iQw99I4isZG6C12vM7Fmm61qhU4EenYN+pP1Zc8dTccmgW/QJzZ5bxoG6dKshxXWh9EuEw2Jw+cSo0TMdL1BcYyLCClDz0IToMr6YQQSdBSITm935Lr+TI35FpoJ7aTHuwSRc6qp7VOrqrGfUFTHFr7LnEOSIUqEu6D0nKBuH1+fCXV54zWwXgDZo1/9+99Or8knnF6dqbDvau85ifweJ5H3xSfRQBR2PV8Gp3BiMSj+NN3hs7ps+/Ovq1bVbmuAfxIhqf4Fa+TO50aeeCM37Hhpq7PjwWz79pc2Gc4dZEiAD5merFOi+XaihUMnWudbW90nb8qrVGVbbioSPejEPF57Irc1kYrfmqjHNfKFzx891XwWolQBtLLRP52NA2p58n20HLQuhH217L+REwNK2b679PwOckZSNi9g3kkdxyBLi5W9dwh48FvPOwTcKqKBO5rOjWc87ifo9rzDKVqY9Wdv1b3+z0Kw/Ac=</diagram></mxfile>" style="background-color: rgb(255, 255, 255);"> + <defs/> + <g> + <rect x="19.5" y="19.5" width="600" height="60" fill="#e6e6e6" stroke="#666666" stroke-width="1.5" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 398px; height: 1px; padding-top: 33px; margin-left: 14px;"> + <div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"> + Backends + </div> + </div> + </div> + </foreignObject> + <text x="213" y="37" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle"> + Backends + </text> + </switch> + </g> + <rect x="259.5" y="154.5" width="120" height="60" fill="#e6e6e6" stroke="#666666" stroke-width="1.5" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 123px; margin-left: 174px;"> + <div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"> + Services + </div> + </div> + </div> + </foreignObject> + <text x="213" y="127" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle"> + Services + </text> + </switch> + </g> + <path d="M 139.5 274.5 L 250.32 274.5" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/> + <path d="M 257.82 274.5 L 250.32 277 L 250.32 272 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 183px; margin-left: 133px;"> + <div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"> + Provide + </div> + </div> + </div> + </foreignObject> + <text x="133" y="186" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle"> + Provide + </text> + </switch> + </g> + <path d="M 109.5 244.5 L 250.98 187.91" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/> + <path d="M 257.94 185.12 L 251.91 190.23 L 250.05 185.59 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 143px; margin-left: 123px;"> + <div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"> + Use + </div> + </div> + </div> + </foreignObject> + <text x="123" y="146" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle"> + Use + </text> + </switch> + </g> + <rect x="19.5" y="244.5" width="120" height="60" fill="#e6e6e6" stroke="#666666" stroke-width="1.5" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 183px; margin-left: 14px;"> + <div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"> + Plugins + </div> + </div> + </div> + </foreignObject> + <text x="53" y="187" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle"> + Plugins + </text> + </switch> + </g> + <rect x="259.5" y="244.5" width="120" height="60" fill="#e6e6e6" stroke="#666666" stroke-width="1.5" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 183px; margin-left: 174px;"> + <div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"> + Extension Points + </div> + </div> + </div> + </foreignObject> + <text x="213" y="187" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle"> + Extension Poi... + </text> + </switch> + </g> + <path d="M 499.5 274.5 L 388.68 274.5" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/> + <path d="M 381.18 274.5 L 388.68 272 L 388.68 277 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 183px; margin-left: 293px;"> + <div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"> + Call + </div> + </div> + </div> + </foreignObject> + <text x="293" y="186" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle"> + Call + </text> + </switch> + </g> + <path d="M 529.5 244.5 L 388.02 187.91" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/> + <path d="M 381.06 185.12 L 388.95 185.59 L 387.09 190.23 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 143px; margin-left: 303px;"> + <div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"> + Use + </div> + </div> + </div> + </foreignObject> + <text x="303" y="146" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle"> + Use + </text> + </switch> + </g> + <rect x="499.5" y="244.5" width="120" height="60" fill="#e6e6e6" stroke="#666666" stroke-width="1.5" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 183px; margin-left: 334px;"> + <div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"> + Modules + </div> + </div> + </div> + </foreignObject> + <text x="373" y="187" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle"> + Modules + </text> + </switch> + </g> + <path d="M 78.9 81.9 L 79.47 235.32" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/> + <path d="M 79.49 242.82 L 76.97 235.33 L 81.97 235.31 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 109px; margin-left: 53px;"> + <div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"> + Install + </div> + </div> + </div> + </foreignObject> + <text x="53" y="112" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle"> + Install + </text> + </switch> + </g> + <path d="M 559.5 80.7 L 559.5 235.32" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/> + <path d="M 559.5 242.82 L 557 235.32 L 562 235.32 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 108px; margin-left: 373px;"> + <div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"> + Install + </div> + </div> + </div> + </foreignObject> + <text x="373" y="111" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle"> + Install + </text> + </switch> + </g> + <path d="M 319.5 79.5 L 319.5 145.32" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" stroke-dasharray="4.5 4.5" pointer-events="stroke"/> + <path d="M 319.5 152.82 L 317 145.32 L 322 145.32 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/> + <g transform="translate(-0.5 -0.5)scale(1.5)"> + <switch> + <foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"> + <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 78px; margin-left: 213px;"> + <div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"> + <div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"> + Provide + </div> + </div> + </div> + </foreignObject> + <text x="213" y="81" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle"> + Provide + </text> + </switch> + </g> + </g> + <switch> + <g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/> + <a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"> + <text text-anchor="middle" font-size="10px" x="50%" y="100%"> + Text is not SVG - cannot display + </text> + </a> + </switch> +</svg> diff --git a/docs/assets/header.png b/docs/assets/header.png new file mode 100644 index 0000000000..3c39971d36 Binary files /dev/null and b/docs/assets/header.png differ diff --git a/docs/assets/permissions/permission-framework-overview.drawio.svg b/docs/assets/permissions/permission-framework-overview.drawio.svg index c7e628f8a6..3740384748 100644 --- a/docs/assets/permissions/permission-framework-overview.drawio.svg +++ b/docs/assets/permissions/permission-framework-overview.drawio.svg @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- Do not edit this file with editors other than diagrams.net --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="643px" height="222px" viewBox="-0.5 -0.5 643 222" content="<mxfile host="app.diagrams.net" modified="2022-02-08T17:09:33.344Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36" etag="6rJwh-3Z6puxa77opBg4" version="16.5.4" type="google"><diagram id="TTnOLjRJzGgt66t77H9m" name="Page-1">5VtNc6M4EP01rto92IXA+OMYO5ndqclWpZKamsxRNjJmgxEj5NjeX78SSGAhgUlsSDI5BTVSI7/ufq1uSM+Zb/Z/ERiv/8EeCnu25e17znXPtgGwp+wPlxwyydhyMoFPAk9MKgQPwX9ICC0h3QYeSpSJFOOQBrEqXOIoQkuqyCAheKdOW+FQfWoMfaQJHpYw1KU/Ao+u5e8aTYsbf6PAX4tHT+xxdmMD5WTxS5I19PDuSOTc9Jw5wZhmV5v9HIUcPIlLtu5Lxd18YwRFtMkC/2bx7d5/7NN+fPtjsv9+h/xFX2h5huFW/ODvCSJiw/QgUSB4G3mIK7J6zmy3Dih6iOGS390xuzPZmm5CNgLsUqhEhKJ95V5BjgBzHYQ3iJIDm7KXXpKtEE4zFMNdYYGJEK2PsJfToLC5n+stYGEXApkXoGRrKN2FWz+IzsOJIxQwT7sKAz9iMor5hBWO6IPQCC4Ape2exBLYBjDBuC00HUuHE5FNkCQBZpBa9yiJcZQgDV3ksdAUQ0zoGvs4guFNIZ2p+BdzbjHHNkX9X0TpQfAM3FKs2gTtA/oolvPrn/x6MBnLe9f7o5vXBzmIGC6PUgUfZOvGrhwX69KRXJgaO9sLkFoZFgEDmkVhNieDgf/2eg9gUOEtWaI65M2eQlAIafCs6jeZXSy9wwF7cu5h7lT1MDCZDFxVSbYzse6Yo0qqnPFJVRQSH1FNVeqJ+W86wznrffMLgRu0w+Sp/dB3Zgkl+ClPO3YumeMQk/SpjmVNp5Z1GdJ1ndNM4XbKFK5mDJ0TIu+Kp3k2WoaQmWmpIq2aRYY3UMLbrQ3uC8ZfBf5HAJvwlbIzwxRMVPOOSlZrGqPlhFLW03KAjjSfuFrSNDhHIQNstiCKg4x+bfkBK42tfpJS7RWbMI33qWnlbXbl879/3CPopVj/2qKE/mnMQbdwwU65iptBEchL5hOcubUI3wSel6UoxHYBF6k+7l4xByqFzp313Gs1KUzrIlsceYWuXn7QPPbM6rCqpAFLMq6wr30R7+uXyKWkAK9WCWrFX8aav7DzRerqyXkcfomzWSndjZudc93Wzrnu+zlz1ZNy+eB0QZKetHJIAtOR0dQnWFNn35GqB7jNjlosScLD0TRBO433K59TuFem8aKxKmvnt/G/lxwKPpz/DV0z1XSUtScaC8t8zZFskrGBxVK2lq8ll6cApZk7PjquZ09gG84eki05j/Ubncktaz5vJ0MA541TxFQzpFIf3eEwWB7Og7gcWxrA8zmH2GiMC0Dujk4m5akBcbstxIHeLtFZ8kVVkNZ1kO2LvjWwgK32MEAhqGhipKPXdy0y5qkBoKJr0U3VVO5IuNNG6Vbn33KTpFv+BXqjt9RzS+sdY/rtrtwBtZ2MV9c7wDJ7UF7wWCPrQlXOUDGybBy3X+WA4WmS8BkLxGehW0+cw5KHT3TiBMNOmdM+DcqLMlPII4EdMT1ETOmoPnFFOELGnLUKwvDVLb0ab9ANJTlMs4vxBUBrZjH0WAl+DrgZbGvBNzqDy6eE8ldzZXsxLKiZi0K0oucxES8NEmG/KvMdG0uKmhLYq+0mc1HDVzftBVSDhmzHLGNbBlBGnYKidyQ/AMvk6s7w1oo+4jthGb3xN8fRKvC3pOAZxgDIJ5BiojcDf2+mqe8Bm5jG6TSoTA2DN2Ya2wDKuFNQ9OL7AzDNy9ohNd7wXplGPqyWaWLxwYjFQn/9+eimwoA1dNNppZA3Z9/mxUfebP55dKfzxrMgk5P9GNtsy8al83kfD+k1ndLI4JUDirzjt8FZ21eZtcb4KdHnfBXHATEJhTEihmmDweDivFsRctUffNTzrodWcBvSpkF7oiM6VN8IA2usx6uJcsufBlzOC/QS0hDBYRjECTptDpjE2aejq2DPTdjQPpXN/67tAyalN/Y6nXZaJjp6M0o/qHwa6wy1ivWNraMX8c7ntc7IVWPH8LanW+vo3YTh57WOHjutmYcNi2/xs7Z88R8Nzs3/</diagram></mxfile>" style="background-color: rgb(255, 255, 255);"><defs/><g><rect x="31" y="21" width="80" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 41px; margin-left: 32px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">User</div></div></div></foreignObject><text x="71" y="45" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">User</text></switch></g><rect x="211" y="21" width="120" height="170" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 28px; margin-left: 212px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; overflow-wrap: normal;">Plugin</div></div></div></foreignObject><text x="271" y="40" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle" font-weight="bold">Plugin</text></switch></g><path d="M 491 169.07 L 411.04 169.13 L 337.37 169.47" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 332.12 169.49 L 339.1 165.96 L 337.37 169.47 L 339.13 172.96 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 169px; margin-left: 411px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">Permission Response</div></div></div></foreignObject><text x="411" y="172" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px" text-anchor="middle">Permission Response</text></switch></g><rect x="491" y="21" width="150" height="170" fill="rgb(255, 255, 255)" stroke="#009900" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 28px; margin-left: 492px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; overflow-wrap: normal;">Permission Framework</div></div></div></foreignObject><text x="566" y="40" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle" font-weight="bold">Permission Framework</text></switch></g><path d="M 111 41 L 204.63 41" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 209.88 41 L 202.88 44.5 L 204.63 41 L 202.88 37.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 40px; margin-left: 156px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">Action<br style="font-size: 9px" />(Read request)</div></div></div></foreignObject><text x="156" y="43" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle">Action...</text></switch></g><rect x="231" y="51" width="80" height="50" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 76px; margin-left: 232px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Resources</div></div></div></foreignObject><text x="271" y="80" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Resources</text></switch></g><path d="M 231 136 L 156.96 136 L 157 57.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 157 52.12 L 160.5 59.12 L 157 57.37 L 153.5 59.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 311 136 L 411.04 136 L 411 57.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 411 52.12 L 414.5 59.12 L 411 57.37 L 407.5 59.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="231" y="111" width="80" height="50" fill="rgb(255, 255, 255)" stroke="#0000cc" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 136px; margin-left: 232px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 10px">Resource read permission</font></div></div></div></foreignObject><text x="271" y="140" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Resource read...</text></switch></g><rect x="521" y="51" width="90" height="20" fill="rgb(255, 255, 255)" stroke="#cc0000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 61px; margin-left: 522px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Permission Policy</div></div></div></foreignObject><text x="566" y="64" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px" text-anchor="middle">Permission Policy</text></switch></g><path d="M 331 40 L 482.83 40.04" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 488.08 40.04 L 481.08 43.54 L 482.83 40.04 L 481.08 36.54 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 38px; margin-left: 411px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">Permission Request</div></div></div></foreignObject><text x="411" y="41" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px" text-anchor="middle">Permission Request</text></switch></g><rect x="1" y="166" width="20" height="10" fill="#009900" stroke="none" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 171px; margin-left: 33px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: nowrap;">Provided by Backstage</div></div></div></foreignObject><text x="33" y="174" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px">Provided by Backstage</text></switch></g><rect x="1" y="186" width="20" height="10" fill="#cc0000" stroke="none" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 191px; margin-left: 33px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: nowrap;">Configured by integrators</div></div></div></foreignObject><text x="33" y="194" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px">Configured by integrators</text></switch></g><rect x="1" y="206" width="20" height="10" fill="#0000cc" stroke="none" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 211px; margin-left: 33px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: nowrap;">Configured by plugin authors</div></div></div></foreignObject><text x="33" y="214" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px">Configured by plugin authors</text></switch></g><rect x="506" y="88" width="120" height="60" fill="rgb(255, 255, 255)" stroke="#009900" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 118px; margin-left: 507px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">Permission Backend<br />Permission hooks<br />Integration helpers<br />...</div></div></div></foreignObject><text x="566" y="121" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px" text-anchor="middle">Permission Backend...</text></switch></g><ellipse cx="156" cy="11" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="#000000" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 18px; height: 1px; padding-top: 11px; margin-left: 147px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">1</div></div></div></foreignObject><text x="156" y="15" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">1</text></switch></g><ellipse cx="411" cy="11" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="#000000" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 18px; height: 1px; padding-top: 11px; margin-left: 402px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">2</div></div></div></foreignObject><text x="411" y="15" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">2</text></switch></g><ellipse cx="626" cy="61" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="#000000" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 18px; height: 1px; padding-top: 61px; margin-left: 617px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">3</div></div></div></foreignObject><text x="626" y="65" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">3</text></switch></g><ellipse cx="411" cy="191" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="#000000" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 18px; height: 1px; padding-top: 191px; margin-left: 402px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">4</div></div></div></foreignObject><text x="411" y="195" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">4</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="642px" height="240px" viewBox="-0.5 -0.5 642 240" content="<mxfile host="Electron" modified="2023-02-03T01:42:11.939Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.8.14 Chrome/106.0.5249.199 Electron/21.4.0 Safari/537.36" etag="b7d8TJtL8TzOSDB05xRz" version="20.8.14" type="device"><diagram id="TTnOLjRJzGgt66t77H9m" name="Page-1">5Vvfc6o4FP5rnNl9sENAUB6r7f0x293ptHun7b6hRGSLhIbY6v71m0AihETAqnh7+6SEkMTzne87OSfYsybL9VfsJYs/kQ+jnmn465511TNNYDkG/WAtm7xlNLDyhgCHPu9UNNyH/0HeyJ8LVqEPU6kjQSgiYSI3zlAcwxmR2jyM0ZvcbY4iedbEC6DScD/zIrX1IfTJgrcCxy1ufINhsOBTj8xhfmPpic78l6QLz0dvpSbrumdNMEIk/7ZcT2DEjCfskj/3Zcfd7cIwjEmbB4Lr6R93wWOf9JObh9H6xy0Mpn0+yqsXrfgP/pFCzBdMNsIKGK1iH7KBjJ41fluEBN4n3ozdfaO407YFWUb0CtCv6sLELBATuC418YV+hWgJCd7QLvzukNuMO82AX74VCIx406Jke9HN45gH23ELs9Av3DJ6K12v//pu3W7Ayz/99bX79O3vmwckrFQyCPSpl/BLhMkCBSj2ouuidSybrOhzg1DCDfUvJGTDXd5bESSbkVoPbx7Z8xe2uHwq37ta88Hzqw2/ytfKFrjTUcTvQSs8gzXeYXLCeTiApMmLVDAxjDwSvsrr0IGTPXqJsbcpdUhQGJO0NPItayh8BADZSYBrVIDORyxg3y6tlSfUWaTEl9toFYTxYYxhxAip5lxGYRDTNsJcZDxHMRE+thvE9qQy7UZWAVNDKzA8Aq+01rQM1ZwQL8M0DRE1qXEH0wTFKTwL/dYheRQEo9+fMiaOhuJewT52Ici3JS0oU9a4GNoNrM3AztcCxKjUFiE1NNXjMrPfQ2RLKwqHMlahpO1WKDkaUe2SBsmXyp8rR6vKUNawcahclpShjkV1q943v2BvCd8Qfj499a1xSjB63m5AzG3LBEUIZ7NahuG6Rq2XtFcK22pWCrtTpbAVMFRNiP1LtuGjV7PIozDNZEvLsAh6A4nedi25388/0JJ/JQPr7CvaDqQpGMnwOhXU2nK0GlCq45yYoI7iE5czkpHTiajBxlMsOYjzsmJb7Yxb/TST2kvawU3WGbTiNv0WsM/f7qDnZ7Z+WcGU/K6NQTfelOY7kpt5nMgz6iRMuRWGL0Pfz0MUpKvwptl4zL34hocObo979pUcFFyt/9VypUr3bZLE5+yV8xCdDBhCcTm+5lG8r18Rl8oAaD5P4Un8Zaj4C91fZK6eHqbhx9ibVcLdsF3GY59Kb03759lz1YtydeP0fpEedbNJAq6jhbpBNVX1deRxgN1uq7V3nuXq5zltnjU8p//tsyn4cP43sPVS01HUHikqLOI1s2SbiA0MGrKVeC20PDNQFrmT0nY9n4EuOJ8kf+Qw1W+1JzeMyeQ0EQJYZw4RrgKklB/doiicbQ4zcZVbioEnE2ZiLRhHMLntNAZlV2Nx81QWB2q5RFXJvbIgpeogyhd948IAplzDAEXDjiJGdtW6atFYVmxbtegma6pWJGy3VbhV9bdaJOlWf4Fa8q/U3LJ8Rxt+u0t3doTSelocIeExHONIWc5AAlkUjk+f5YBBs0gEVAWS9gK5tx0HFQ93R4pwgkGnymk2G2WvyBQxJtAtpg+xLhzVB64YxVAbs+ZhFLUr6dVD3xzhhIYpuGgPAE4Gi6bGitFryGAwjSlb6NibPaeEHdJW8aI/kei1KIJzcpgSsdQg5fjtgq8Mlmh6p4C1x03EopZHN6cjVIuCbMcqYwKNyjidGkWtSH4AldkO19Zbd9UXf1qVUQt/ExTPw2CFC52hCgAD7BGE1WLgL6Q07bGrURqrU1LpCgZnVhpLozTDTo2iJt8fQGlqyiH10H8UpRGT1SpNwl8YMSj1F7+43LQHsEZuOs0UttXa8xx8bIvNT6U7py48c+1orMfwfscrUB/28pCa00mFDJY5wNgvnwbnZV+p1wKh51Tt851vB3gnGCUQa7pdXFwcXXd3UG73Cx/1uuvDubeKSJ2D7FERHcgnwsAYqnzVSW711YDjeYGaQmoYHEVhksJmOLw0yV8inodrBmFLfHYW/7vGB4wqJ/aqnHaaJlpqMUrdqHwadAbVfeS50VGTeOvzouPYMnc0pz3doqNWEwafFx2VO93Bo/1PgLr70Lwy84HQ0WTJCmA70amEHeC0S8ROBo4qbPnfWgy6wU3ZhNkutebl8nO82Ne6TrEPMttHtju2GpvvOMqyh7IwVg4+33OWRS+Lfz7l3Yv/j1nX/wM=</diagram></mxfile>"><defs/><g><rect x="31" y="21" width="80" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 41px; margin-left: 32px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">User</div></div></div></foreignObject><text x="71" y="45" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">User</text></switch></g><path d="M 211 171 L 71 171 L 71 67.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 71 62.12 L 74.5 69.12 L 71 67.37 L 67.5 69.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="211" y="21" width="120" height="170" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 28px; margin-left: 212px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; overflow-wrap: normal;">Plugin</div></div></div></foreignObject><text x="271" y="40" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle" font-weight="bold">Plugin</text></switch></g><path d="M 491 169.07 L 411 169.1 L 337.37 169.47" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 332.12 169.49 L 339.1 165.96 L 337.37 169.47 L 339.14 172.96 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 170px; margin-left: 411px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">Permission Response</div></div></div></foreignObject><text x="411" y="173" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px" text-anchor="middle">Permission Response</text></switch></g><rect x="491" y="21" width="150" height="170" fill="rgb(255, 255, 255)" stroke="#009900" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 28px; margin-left: 492px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; overflow-wrap: normal;">Permission Framework</div></div></div></foreignObject><text x="566" y="40" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle" font-weight="bold">Permission Framework</text></switch></g><path d="M 111 41 L 204.63 41" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 209.88 41 L 202.88 44.5 L 204.63 41 L 202.88 37.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 41px; margin-left: 157px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">Action<br style="font-size: 9px;" />(Read request)</div></div></div></foreignObject><text x="157" y="43" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle">Action...</text></switch></g><rect x="231" y="51" width="80" height="50" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 76px; margin-left: 232px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Resources</div></div></div></foreignObject><text x="271" y="80" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Resources</text></switch></g><path d="M 231 136 L 157 136 L 157 57.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 157 52.12 L 160.5 59.12 L 157 57.37 L 153.5 59.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 311 136 L 411 136 L 411 57.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 411 52.12 L 414.5 59.12 L 411 57.37 L 407.5 59.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="231" y="111" width="80" height="50" fill="rgb(255, 255, 255)" stroke="#0000cc" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 136px; margin-left: 232px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 10px">Resource read permission</font></div></div></div></foreignObject><text x="271" y="140" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Resource read...</text></switch></g><rect x="521" y="51" width="90" height="20" fill="rgb(255, 255, 255)" stroke="#cc0000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 61px; margin-left: 522px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Permission Policy</div></div></div></foreignObject><text x="566" y="64" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px" text-anchor="middle">Permission Policy</text></switch></g><path d="M 331 40 L 482.83 40.04" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 488.08 40.04 L 481.08 43.54 L 482.83 40.04 L 481.08 36.54 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 39px; margin-left: 411px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">Permission Request</div></div></div></foreignObject><text x="411" y="42" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px" text-anchor="middle">Permission Request</text></switch></g><rect x="1" y="184" width="20" height="10" fill="#009900" stroke="none" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 189px; margin-left: 33px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: nowrap;">Provided by Backstage</div></div></div></foreignObject><text x="33" y="192" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px">Provided by Backstage</text></switch></g><rect x="1" y="204" width="20" height="10" fill="#cc0000" stroke="none" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 209px; margin-left: 33px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: nowrap;">Configured by integrators</div></div></div></foreignObject><text x="33" y="212" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px">Configured by integrators</text></switch></g><rect x="1" y="224" width="20" height="10" fill="#0000cc" stroke="none" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 229px; margin-left: 33px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: nowrap;">Configured by plugin authors</div></div></div></foreignObject><text x="33" y="232" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px">Configured by plugin authors</text></switch></g><rect x="506" y="88" width="120" height="60" fill="rgb(255, 255, 255)" stroke="#009900" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 118px; margin-left: 507px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">Permission Backend<br />Permission hooks<br />Integration helpers<br />...</div></div></div></foreignObject><text x="566" y="121" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px" text-anchor="middle">Permission Backend...</text></switch></g><ellipse cx="156" cy="11" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="#000000" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 18px; height: 1px; padding-top: 11px; margin-left: 147px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">1</div></div></div></foreignObject><text x="156" y="15" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">1</text></switch></g><ellipse cx="411" cy="11" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="#000000" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 18px; height: 1px; padding-top: 11px; margin-left: 402px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">2</div></div></div></foreignObject><text x="411" y="15" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">2</text></switch></g><ellipse cx="626" cy="61" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="#000000" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 18px; height: 1px; padding-top: 61px; margin-left: 617px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">3</div></div></div></foreignObject><text x="626" y="65" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">3</text></switch></g><ellipse cx="411" cy="191" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="#000000" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 18px; height: 1px; padding-top: 191px; margin-left: 402px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">4</div></div></div></foreignObject><text x="411" y="195" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">4</text></switch></g><ellipse cx="56" cy="156" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="#000000" stroke-width="2" pointer-events="none"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 18px; height: 1px; padding-top: 156px; margin-left: 47px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">5</div></div></div></foreignObject><text x="56" y="160" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">5</text></switch></g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 161px; margin-left: 125px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; background-color: rgb(255, 255, 255); white-space: nowrap;">User gets a response</div></div></div></foreignObject><text x="125" y="163" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle">User gets a response</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg> \ No newline at end of file diff --git a/docs/assets/software-catalog/software-model-entities.drawio.svg b/docs/assets/software-catalog/software-model-entities.drawio.svg old mode 100755 new mode 100644 index e14cc93e62..d087583245 --- a/docs/assets/software-catalog/software-model-entities.drawio.svg +++ b/docs/assets/software-catalog/software-model-entities.drawio.svg @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- Do not edit this file with editors other than diagrams.net --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="751px" height="342px" viewBox="-0.5 -0.5 751 342" content="<mxfile host="app.diagrams.net" modified="2022-06-10T08:31:52.901Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36" etag="IHw4RdAX8tDURWJRtPTF" version="17.4.3" type="device"><diagram id="owJHMEFGrEAKtmmgk64n" name="Page-1">3Vpbk6I4FP41Vs0+aAkRkEdvMztbPds9bdVOz9NWhIhMA2FDvPWv30TCJYCKNI7O9EObnCQn5ly+c05iB0z83ScCw9UXbCOvo/btXQdMO6qq9PtD9sEp+5iiGUZMcIhri0kZYe6+oWSloK5dG0XSRIqxR91QJlo4CJBFJRokBG/laUvsybuG0EElwtyCXpn6zbXpKqYOVSOj/4lcZ5XsrOhmPLKA1qtD8DoQ+3VU8PHwFw/7MOElDhqtoI23ORKYdcCEYEzjlr+bII/LNhFbvO7jkdH0exMU0DoLNn+v6Iv1/C/a9H3w7cfseTv/ryu4bKC3RskxdI/xGy9Yw+GNKfahGyRkxj8dEeei+0SWB2kgvl+fDW9XLkXzEFp8dMush9FW1PdYT2HNpet5E+xhclgLlkukWxajR5TgV5QbsQ1z0e+n++VPnHx9RCja5UhCAp8Q9hElezZFjCbKEMZqiu4207yiCdoqp3VD0KAwNidlnAmcNYTML5F/v0IBBakimxms6GJCV9jBAfRmGXUsyz2b84BxKKT9A1G6F94H1xTLukA7l77w5T1N9L4LZrw93eU7+6QTMAHkFvHu94Qf72TLDr1k3VEtRnhNLHRCWJqAB0gcRE/MEyLkcqtjE112AE3TJcPoKmIzgjxI3Y0MFlVmINg/YZcdK+U9MGSDS2EvYREfRqzKjGlECNznpoV8QiT7d7JVMgcvlxGiJZNMD/wOKwUlK2X6o4/LSlt9gAsWIyT7gp7rBKxtMZ0j5tZj7rEuA+GRGPBd245NGUXuG1wc+HFrEQdnzLVxR5tW2s9pzypiQxpKxC4SHB+xD3U4NFuxh66q9jTZ0sBQZnI9NarnwX6+jyjyfxewN+8N7c2SBmwUosCOHoPbwr7aCPfVi4D/qM+X0KF5iNBrhghwWYg4HDVvSonTvhMOCgvKrl9aoZlGAUCGBfw4ElFqcFL0AqtY3A2CU2uIVU6PTgSen+UteVdJE6YzzqLkPSXvOa1nSeBqWZKuJdlpW1nS9WNeOXMpxbxnJGRaGfXEZJJQPqCe02Mc518f2P8pZGkEjNjaCacBXheurVd+Fk7o9Xp/tBo4bQ0N7UFV4ByqC6Dr7QROTS9krTcPnIPbhkcjHx6VRh5vXM/j6wY9rabHt1zzAFCv5mmtWtFKPs8y/mjto2j09LnSkO6kZBkccdRGJYsO5OjeTkHbVZRipZymEddH87JmS2h+UPElQP7IEvDDIg7YzvMT/0j7ow3BcYsjffSL43qxIFK1mwN7uSYNCd7wK+FjvnqbmqgZ6F8xzWsb9LNKZwCKWZ5x71meWr7H/j3qhDvIGhrUCYZStKC7rxP085Flgv0QB1zOF8UX/k6EApurBJGNa4lawWbxhGvODZHnBujXDSrFYkExbh1UjNpgENgj/oTI0zwPRpFr1fFk9e5KfrWmKys1XbkI/DlNVikyob0ztQTmEUM6cx1VYpQ+slz46HIlvJk4c+Jsvu5n3c0b0DExtX8eKh9eC9Z5xtllpw4YMr3Tn3NaViu0rLbgrpWiKKeANxeFGDUKz8VAKUMbqBBVseBtIqrtcLr/623s6OZjaHwBZhS80DpWcw7T5ICSu8w3pMv8/hmwavJiXIk+101e9LOmfkVAMwoXIYrZENAMUAC0mjcqxy/qq/cpXhkog/7J76Ub4NR81oi/QVPkrPSBMlycfkP7KXc7ta9sZHM/7uQnr3bSXyu800CHkva6RYRvEv1YN/tJUzw9+90YmP0P</diagram></mxfile>" style="background-color: rgb(255, 255, 255);"><defs/><g><rect x="20" y="90" width="150" height="70" fill="#ffe6cc" stroke="#d79b00" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 125px; margin-left: 21px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Domain</b></div></div></div></foreignObject><text x="95" y="129" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Domain</text></switch></g><path d="M 365 250 L 365 166.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 365 161.12 L 368.5 168.12 L 365 166.37 L 361.5 168.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 181px; margin-left: 343px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">partOf</div></div></div></foreignObject><text x="343" y="184" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">partOf</text></switch></g><rect x="290" y="90" width="150" height="70" fill="#ffe6cc" stroke="#d79b00" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 125px; margin-left: 291px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>System</b></div></div></div></foreignObject><text x="365" y="129" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">System</text></switch></g><path d="M 597.5 170 L 597.5 96.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 597.5 91.12 L 601 98.12 L 597.5 96.37 L 594 98.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 120px; margin-left: 561px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">dependsOn</div></div></div></foreignObject><text x="561" y="123" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">dependsOn</text></switch></g><path d="M 560 55 L 500 55 L 500 107.5 L 446.37 107.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 441.12 107.5 L 448.12 104 L 446.37 107.5 L 448.12 111 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 40px; margin-left: 530px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">partOf</div></div></div></foreignObject><text x="530" y="43" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">partOf</text></switch></g><rect x="560" y="20" width="150" height="70" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 55px; margin-left: 561px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Resource</b><br />(e.g. SQL Database, S3 bucket, ...)</div></div></div></foreignObject><text x="635" y="59" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Resource...</text></switch></g><path d="M 672.5 240 L 672.5 302.5 L 446.37 302.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 441.12 302.5 L 448.12 299 L 446.37 302.5 L 448.12 306 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 290px; margin-left: 511px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">consumesAPI</div></div></div></foreignObject><text x="511" y="294" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">consumesAPI</text></switch></g><rect x="290" y="250" width="150" height="70" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 285px; margin-left: 291px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>API</b><br />(e.g. OpenAPI, gRPC API, Avro, Dataset, ...)</div></div></div></foreignObject><text x="365" y="289" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">API...</text></switch></g><path d="M 597.5 240 L 597.5 267.5 L 446.37 267.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 441.12 267.5 L 448.12 264 L 446.37 267.5 L 448.12 271 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 251px; margin-left: 510px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">providesAPI</div></div></div></foreignObject><text x="510" y="254" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">providesAPI</text></switch></g><path d="M 560 205 L 500 205 L 500 142.5 L 446.37 142.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 441.12 142.5 L 448.12 139 L 446.37 142.5 L 448.12 146 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 190px; margin-left: 530px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">partOf</div></div></div></foreignObject><text x="530" y="193" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">partOf</text></switch></g><rect x="560" y="170" width="150" height="70" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 205px; margin-left: 561px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Component</b><br />(e.g. backend service, data pipeline ...)</div></div></div></foreignObject><text x="635" y="209" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Component...</text></switch></g><path d="M 290 107.5 L 176.37 107.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 171.12 107.5 L 178.12 104 L 176.37 107.5 L 178.12 111 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 91px; margin-left: 230px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">partOf</div></div></div></foreignObject><text x="230" y="94" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">partOf</text></switch></g><rect x="0" y="0" width="20" height="20" fill="rgb(255, 255, 255)" stroke="none" pointer-events="all"/><rect x="720" y="310" width="30" height="30" fill="rgb(255, 255, 255)" stroke="none" pointer-events="all"/><path d="M 635 170 L 635 140 L 673 140 L 672.61 163.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 672.52 168.88 L 669.14 161.82 L 672.61 163.63 L 676.13 161.94 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 120px; margin-left: 650px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">dependsOn</div></div></div></foreignObject><text x="650" y="124" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">dependsOn</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="782px" height="834px" viewBox="-0.5 -0.5 782 834" content="<mxfile host="Electron" modified="2023-02-03T01:35:43.413Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.8.14 Chrome/106.0.5249.199 Electron/21.4.0 Safari/537.36" etag="QEfTK5z7qC0nIhM95y51" version="20.8.14" type="device"><diagram id="owJHMEFGrEAKtmmgk64n" name="Page-1">7V1bd6O2Fv41Xqt9cBZ38GNuM53TTJNOpqvpeTlLBtlmAogKOY7n11fCAhtJ2DjGwKQnDwkSQqB9/bS1pYzM6/j1Iwbp4jMKYDQytOB1ZN6MDEM3HY3+YTXrTY1nmZuKOQ4D3mhb8Rh+h7ySPzdfhgHMKg0JQhEJ02qlj5IE+qRSBzBGq2qzGYqqb03BHEoVjz6I5No/w4As+CgMd1v/Cwzni+LNujPZ3JkC/3mO0TLh7xsZ5of8Z3M7BkVffKDZAgRotVNl3o7Ma4wQ2VzFr9cwYrQtyLZ57kPN3fK7MUxIkwf+O53dPLkP3wN4C9DX+6tvn+D9WLf4x5F1QRAYUPrwIsJkgeYoAdHttvYqHzRk3Wq0tG1zh1BKK3Va+Q0SsubMBkuCaNWCxBG/G4BskT/PCvI4+NDYh+xU8FF9hCiGBK9pAwwjQMKXKiMBl4d52a589AGF9BWGxmXXtDknueTahl3tIkNL7EP+1C5lD3TkaEJHBOA5JFJH9GJnPNuqnHFHMNHZvO0FREtYyKLA1CrLVouQwMcU+Ozuiup1lT3zCGQZbzoLo+gaRQjTcoIS2Jh7LxAT+LqXfwX5tCobeHG11UbX43WLHU20XK2e4xUS76Hny28L8uR/+R980WLzz2+3X1aPf491BT2diL72akov5uziBsUgTIpq+pbyTtkUFzU/wYv5BS2AmNE5mWZp3kwLKn1s28fMxmYj45pZETqg0OeFXx8+8avpMgsTSJlESb/EKcrgz9Inyh2rP/cUSdmVDmr/ZjPo+D6tzwhGz3DnTuBOpprWjrzYZkVeDEsWGN1WCMz55EU7rIAdWFX4GpIn9viFzUt/8c7Y9c3rbmFdFBJKgJ2HWPGvoj9W2D6Wl4rnatm4sZl7iMVN48Yk7mlnNHQCXCjGdAC27VQkY6zzl53oKCyzKnEl5jlg3y8xBuudZilrkKldSdEGzWYZPNVLqKXUlKSU8o/cz5SyegemFOhV5AtE4Tyh1z7lOaR6fcVUNqRI6pLfiMMg2IgyzMLvYJr3x6SFD5x2bl+N7Bul/OzXLNE4lHiQv6WCqWrkw/C8SSvyMDaMC7sqaaZX7eR8bDQOO6fHdUZg3Mz+UxsdUTKGiDoiDc2YxiUkJCFkvoUsAMlJjVKIAYE5Nmd8g3iGcJyre8xqZ8uE9/FvcUID9EITSTICmMIkyO6Tft2R8SZ/ZBzlkGptkWS13u66nIauyzzOdeVDrfgXrxUzJTwgmyTpCdvRBMNmiIatxtU16MrUha5qpleHvWZrplTGbXs8YlfqsqsrJZI7oC36rqrsqk7r8M08G3xz7AI2twXf6iW/XsbqEGB1jmrwGX6d9EvtbU8Q4M0XtBoGsNxe5La02FUZPCSCb5mttCq2ahraDeW2sUye5NNlyC4hrC+QD/6YWIT2+Psd/X0DKH4GGdwEFR7NPLLgPzNdYRUXFxc/twrNAht6gaWCZp4xNR2nHWhmuVZF+XS3d2jWS5R1q2Hurorpe1WszqW453MpTWFV66rZbLZvec1m+63N021J6elcN1vGMLt8+KQUpIFM1q0aTX3TZN0xq/CxnVDOWNdEkOFU+2hl5q70LcWAevLP+oXmCC7acQ85aVZ6gDikY2dS1H7sT02ppjOobrywrJCSF8418xgHfE9n5vlDzNHOvzywP2X58gWjzRXz0NkP7o/FUIml9e6P5SBaitELW4iuM7H9BEve5qvPOP1r21dvQyCWKRpm9zSL30EoVl4ofB/xgwGAvTfED1xdlKDBxg9M+8j4wcQUBPgM8QOjn1lKXfzA7RicKCRTTaZhgRNV8ocATq5RnKKEEeQoiMISnGASMNpB/BL6PEwQUEjCSBymMAoT+OPiEjFOYBp94xK3sT9JgkuW+0ZLPkvWCf0mzsDoOpp8UEVEX79DeRXhi7oTJ4HS2p1rXrwx9cvQDnZ1cMWkVaCy8m7W//l+NXcm96n72ZxkyRNRJjQdKVFVdd5ZpnMry3TaIZvdadS3KdhwGoprN+LpiukmlmBvmgqnMxE6ahjJOhbd2FY1/9GYaHu/y/bcfe1PRjdKHZCnfPtXxzuPqe3V3FZCamV+1IkC6lW4Vy6jnj+CVqQw9YVQj8uRG0aWs2VVpxJOEQA91pqIHZXLtR1lOcvgSBKGIWc5G0KOuC6DTdfTZadiOHY9y5uiTfVapy4RsNN4dBVlWlS5zzXfUw7fk8GBOhg9aVtRm3It+u2P6zv3Nfg4vfS+mk9za4IW40GZwENwT0k5Bbtat3ol1OHq5r55b4fYkdft3g5PYfWE6f2vIduCdElbfaSMTs+eYBkA6M2UCZaO78HprB17Wc7GC4PJQ2adzM7V6EOTyDhg9NEf4CgeqdOYxoBD7EjU4TOrnpxPu0f1/sggfqeaVzKwP80bFFI5uDpSBDmKSFzD6JsiAEMrP4RR8UEZFXgiN8qrd5qdHxJ5Z7Ekx4cdBA/hVhZJDrZ3tQ6SMos1qSFsBdu/mvx2sXEbio3btticZlTklZP7VQLxUDZEtRb6sZzqtjjbts5FUnmOzkgaXK3fGUlNIQZmFysKrZNUsUfhsViNOwVZtIATXK0axjV1GSc4Cpggxgvbo5W8zrGC0ywkA6SV1TetjB+HVm7ftJLT8L+uU5idRqlj4o+iFSQMB7RBa0dcivHksKSuKYitm2ejtpxumf3f4tXQSsYwg9Xi3i2eDE6icIoBlsFJ77Tq2+KZMuoA2TrxQRr2TizdENKJFSsp3RJLhh1zdlbW39HwaKXYpd4trWTYMcepPzxCKfaMdUuo94s5dCFnylSc+KTGHPbZqG1J1EYpTIZo7kwFQutWMmXMURxHNV4mIemfYsIxQI5nSRSbdEoxRV4nRsHSJ2M6TjA4grl63wSTVz3zcxIHRyirb0LJa1TvxkvY1dU/pwi59eclLBkVEwjiwYmlY/cslpaM8jJzXBwtMLDJVhnJ6MuhWjLU86NlxuLfg6OV0zet5ODQezF4YihOdWqF0t6dj9gy0gvKE0OGJpmKTcWdSqYtO4c96SJfYZxG+Yl6TXZkPQAM8qRHdigfJVoAMSU1/SB2aB9ZQPmJGUYJ2WzeAvnvvBVlGExZH/AV+ktS9iEdTctb+2DGztMOkzljF0Z+fuRs/0f8QT2woTtS5MFMHNcELe0PE3YJKUyfMgtmcjYJU23jqZWwO+SD3SMZ90sY29EXA/zMfE5x7COGMypniZ8fBYmoSLB7VGo3FflBkBFCzyN26Dq7Rd8HIsREZbNF8N8iJ+VR9j0IijJFWEZfMYyn/WY2NF5zb5QdXGYIVpfh3X2ZDeqkICHHsNhw0+peFiWT5IjTAmSfcz69My6JyRKuvWdHw8C4JMPNzbjfnyoJee6O8+OokgxT/UUYBe+QR0JCtD4EHtHi9p+YbJpv/1OMefsP</diagram></mxfile>" style="background-color: rgb(255, 255, 255);"><defs/><g><path d="M 330 475 L 330 548.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/><path d="M 330 553.88 L 326.5 546.88 L 330 548.63 L 333.5 546.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="1" y="0" width="780" height="470" fill="none" stroke="rgb(0, 0, 0)" stroke-dasharray="3 3" pointer-events="all"/><rect x="24" y="190" width="150" height="70" fill="#ffe6cc" stroke="#d79b00" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 225px; margin-left: 25px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Domain</b><br />(e.g.  domain<br />models, metrics, KPIs, business purpose)<b><br /></b></div></div></div></foreignObject><text x="99" y="229" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Domain...</text></switch></g><path d="M 299 350 L 299 266.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 299 261.12 L 302.5 268.12 L 299 266.37 L 295.5 268.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 281px; margin-left: 277px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">partOf</div></div></div></foreignObject><text x="277" y="284" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">partOf</text></switch></g><rect x="224" y="190" width="150" height="70" fill="#ffe6cc" stroke="#d79b00" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 225px; margin-left: 225px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>System<br /></b>Collection of entities that cooperate to perform some function<b><br /></b></div></div></div></foreignObject><text x="299" y="229" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">System...</text></switch></g><path d="M 482.5 270 L 482.5 196.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 482.5 191.12 L 486 198.12 L 482.5 196.37 L 479 198.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 220px; margin-left: 446px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">dependsOn</div></div></div></foreignObject><text x="446" y="223" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">dependsOn</text></switch></g><path d="M 445 155 L 401 155 L 401 207.5 L 380.37 207.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 375.12 207.5 L 382.12 204 L 380.37 207.5 L 382.12 211 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 140px; margin-left: 424px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">partOf</div></div></div></foreignObject><text x="424" y="143" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">partOf</text></switch></g><path d="M 520 120 L 520 80 L 651.63 80" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 656.88 80 L 649.88 83.5 L 651.63 80 L 649.88 76.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="445" y="120" width="150" height="70" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 155px; margin-left: 446px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Resource</b><br />(e.g. SQL Database, S3 bucket, ...)</div></div></div></foreignObject><text x="520" y="159" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Resource...</text></switch></g><path d="M 557.5 340 L 557.5 402.5 L 380.37 402.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 375.12 402.5 L 382.12 399 L 380.37 402.5 L 382.12 406 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 392px; margin-left: 428px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">consumesAPI</div></div></div></foreignObject><text x="428" y="395" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">consumesAPI</text></switch></g><path d="M 224 385 L 199 385 L 186.37 385.03" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 181.12 385.05 L 188.11 381.53 L 186.37 385.03 L 188.13 388.53 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="224" y="350" width="150" height="70" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 385px; margin-left: 225px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>API</b><br />(e.g. OpenAPI, gRPC API, Avro, Dataset, ...)</div></div></div></foreignObject><text x="299" y="389" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">API...</text></switch></g><path d="M 482.5 340 L 482.5 367.5 L 380.37 367.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 375.12 367.5 L 382.12 364 L 380.37 367.5 L 382.12 371 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 351px; margin-left: 425px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">providesAPI</div></div></div></foreignObject><text x="425" y="354" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">providesAPI</text></switch></g><path d="M 445 305 L 401 305 L 401 242.5 L 380.37 242.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 375.12 242.5 L 382.12 239 L 380.37 242.5 L 382.12 246 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 290px; margin-left: 423px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">partOf</div></div></div></foreignObject><text x="423" y="293" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">partOf</text></switch></g><path d="M 595 305.01 L 651.63 305.01" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 656.88 305.01 L 649.88 308.51 L 651.63 305.01 L 649.88 301.51 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="445" y="270" width="150" height="70" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 305px; margin-left: 446px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Component</b><br />(e.g. backend service, data pipeline ...)</div></div></div></foreignObject><text x="520" y="309" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Component...</text></switch></g><path d="M 224 223.5 L 180.37 223.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 175.12 223.5 L 182.12 220 L 180.37 223.5 L 182.12 227 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 207px; margin-left: 199px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">partOf</div></div></div></foreignObject><text x="199" y="210" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">partOf</text></switch></g><path d="M 520 270 L 520 240 L 558 240 L 557.61 263.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 557.52 268.88 L 554.14 261.82 L 557.61 263.63 L 561.13 261.94 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 221px; margin-left: 535px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">dependsOn</div></div></div></foreignObject><text x="535" y="224" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">dependsOn</text></switch></g><path d="M 411 552 L 411 478.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/><path d="M 411 473.12 L 414.5 480.12 L 411 478.37 L 407.5 480.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="0" y="560" width="781" height="265" fill="none" stroke="rgb(0, 0, 0)" stroke-dasharray="3 3" pointer-events="all"/><path d="M 291 639.95 L 228.37 639.95" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 223.12 639.95 L 230.12 636.45 L 228.37 639.95 L 230.12 643.45 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 351 675 L 351 728.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 351 733.88 L 347.5 726.88 L 351 728.63 L 354.5 726.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="291" y="605" width="150" height="70" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 640px; margin-left: 292px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Kind: Group</b></div></div></div></foreignObject><text x="366" y="644" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Kind: Group</text></switch></g><path d="M 392 735 L 392 681.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 392 676.12 L 395.5 683.12 L 392 681.37 L 388.5 683.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="291" y="735" width="150" height="70" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 770px; margin-left: 292px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Kind: User</b></div></div></div></foreignObject><text x="366" y="774" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Kind: User</text></switch></g><path d="M 447.37 622.5 L 511 622.5 L 511 657.5 L 447.37 657.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 442.12 622.5 L 449.12 619 L 447.37 622.5 L 449.12 626 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 442.12 657.5 L 449.12 654 L 447.37 657.5 L 449.12 661 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 505px; margin-left: 438px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OwnerOf</div></div></div></foreignObject><text x="438" y="508" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">OwnerOf</text></switch></g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 504px; margin-left: 300px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OwnedBy</div></div></div></foreignObject><text x="300" y="507" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">OwnedBy</text></switch></g><rect x="678" y="265" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 275px; margin-left: 679px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Service</div></div></div></foreignObject><text x="708" y="279" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Service</text></switch></g><rect x="678" y="295" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 305px; margin-left: 679px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">website</div></div></div></foreignObject><text x="708" y="309" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">website</text></switch></g><rect x="678" y="325" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 335px; margin-left: 679px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">website</div></div></div></foreignObject><text x="708" y="339" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">website</text></switch></g><rect x="658" y="230" width="100" height="130" fill="none" stroke="rgb(0, 0, 0)" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 237px; margin-left: 659px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Types</div></div></div></foreignObject><text x="708" y="249" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Types</text></switch></g><rect x="678" y="265" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 275px; margin-left: 679px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">service</div></div></div></foreignObject><text x="708" y="279" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">service</text></switch></g><rect x="678" y="295" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 305px; margin-left: 679px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">website</div></div></div></foreignObject><text x="708" y="309" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">website</text></switch></g><rect x="678" y="325" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 335px; margin-left: 679px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">library</div></div></div></foreignObject><text x="708" y="339" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">library</text></switch></g><rect x="94" y="360" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 370px; margin-left: 95px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">asyncapi</div></div></div></foreignObject><text x="124" y="374" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">asyncapi</text></switch></g><rect x="94" y="390" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 400px; margin-left: 95px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">graphql</div></div></div></foreignObject><text x="124" y="404" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">graphql</text></switch></g><rect x="94" y="420" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 430px; margin-left: 95px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">grpc</div></div></div></foreignObject><text x="124" y="434" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">grpc</text></switch></g><rect x="74" y="300" width="100" height="150" fill="none" stroke="rgb(0, 0, 0)" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 307px; margin-left: 75px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Types</div></div></div></foreignObject><text x="124" y="319" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Types</text></switch></g><rect x="94" y="330" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 340px; margin-left: 95px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">openapi</div></div></div></foreignObject><text x="124" y="344" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">openapi</text></switch></g><rect x="127" y="634" width="90" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 644px; margin-left: 128px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">business-unit</div></div></div></foreignObject><text x="172" y="648" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">business-unit</text></switch></g><rect x="127" y="664" width="90" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 674px; margin-left: 128px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">product-area</div></div></div></foreignObject><text x="172" y="678" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">product-area</text></switch></g><rect x="127" y="694" width="90" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 704px; margin-left: 128px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">root</div></div></div></foreignObject><text x="172" y="708" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">root</text></switch></g><rect x="122" y="578" width="100" height="150" fill="none" stroke="rgb(0, 0, 0)" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 585px; margin-left: 123px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Types</div></div></div></foreignObject><text x="172" y="597" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Types</text></switch></g><rect x="127" y="604" width="90" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 614px; margin-left: 128px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">team</div></div></div></foreignObject><text x="172" y="618" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">team</text></switch></g><rect x="678" y="80" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 90px; margin-left: 679px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">s3-bucket</div></div></div></foreignObject><text x="708" y="94" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">s3-bucket</text></switch></g><rect x="678" y="110" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 120px; margin-left: 679px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">cluster</div></div></div></foreignObject><text x="708" y="124" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">cluster</text></switch></g><rect x="658" y="20" width="100" height="120" fill="none" stroke="rgb(0, 0, 0)" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 27px; margin-left: 659px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Types</div></div></div></foreignObject><text x="708" y="39" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Types</text></switch></g><rect x="678" y="50" width="60" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 60px; margin-left: 679px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">database</div></div></div></foreignObject><text x="708" y="64" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">database</text></switch></g><rect x="24" y="10" width="150" height="90" fill="#e1d5e7" stroke="#9673a6" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 55px; margin-left: 25px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Kind: Template</b><br />Parameters rendered in the<br />frontend and the steps executed in  the scaffolding process<b><br /></b></div></div></div></foreignObject><text x="99" y="59" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Kind: Template...</text></switch></g><rect x="181" y="10" width="150" height="90" fill="#e1d5e7" stroke="#9673a6" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 55px; margin-left: 182px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><b>Kind: Location</b><br />a marker that references other places to look for catalog data<b><br /></b></div></div></div></foreignObject><text x="256" y="59" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Kind: Location...</text></switch></g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 704px; margin-left: 421px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">memberOf</div></div></div></foreignObject><text x="421" y="707" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">memberOf</text></switch></g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 705px; margin-left: 321px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">hasMember</div></div></div></foreignObject><text x="321" y="708" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">hasMember</text></switch></g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 614px; margin-left: 473px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">parentOf</div></div></div></foreignObject><text x="473" y="617" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">parentOf</text></switch></g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 664px; margin-left: 474px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">childOf</div></div></div></foreignObject><text x="474" y="667" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">childOf</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg> \ No newline at end of file diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index 36859dab45..b03201893c 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -61,7 +61,7 @@ how this is done. Note that for the Bitbucket provider, you'll want to use factory. The `@backstage/plugin-auth-backend` plugin also comes with two built-in -resolves that can be used if desired. The first one is the +resolvers that can be used if desired. The first one is the `bitbucketUsernameSignInResolver`, which identifies users by matching their Bitbucket username to `bitbucket.org/username` annotations of `User` entities in the catalog. Note that you must populate your catalog with matching entities or diff --git a/docs/auth/bitbucketServer/provider.md b/docs/auth/bitbucketServer/provider.md new file mode 100644 index 0000000000..96c7db263e --- /dev/null +++ b/docs/auth/bitbucketServer/provider.md @@ -0,0 +1,52 @@ +--- +id: provider +title: Bitbucket Server Authentication Provider +sidebar_label: Bitbucket Server +description: Adding Bitbucket Server OAuth as an authentication provider in Backstage +--- + +The Backstage `core-plugin-api` package comes with a Bitbucket Server authentication provider that can authenticate +users using Bitbucket Server. This does **NOT** work with Bitbucket Cloud. + +## Create an Application Link in Bitbucket Server + +To add Bitbucket Server authentication, you must create an outgoing application link. Follow the steps described in +the [Bitbucket Server documentation](https://confluence.atlassian.com/bitbucketserver/configure-an-outgoing-link-1108483656.html) +to create one. + +## Configuration + +The provider configuration can then be added to your `app-config.yaml` under the root `auth` configuration: + +```yaml +auth: + environment: development + providers: + bitbucketServer: + development: + host: bitbucket.org + clientId: ${AUTH_BITBUCKET_SERVER_CLIENT_ID} + clientSecret: ${AUTH_BITBUCKET_SERVER_CLIENT_SECRET} +``` + +The Bitbucket Server provider is a structure with two configuration keys: + +- `clientId`: The client ID that was generated by Bitbucket, e.g. `b0f868455c15dcdff5c5fb5d173ae684`. +- `clientSecret`: The client secret tied to the generated client ID. + +## Adding the provider to the Backstage frontend + +To add the provider to the frontend, add the `bitbucketServerAuthApi` reference and `SignInPage` component as shown +in [Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). + +## Using Bitbucket Server for sign-in + +In order to use the Bitbucket Server provider for sign-in, you must configure it with a `signIn.resolver`. See +the [Sign-In Resolver documentation](../identity-resolver.md) for more details on how this is done. Note that for the +Bitbucket Server provider, you'll want to use `bitbucketServer` as the provider ID, +and `providers.bitbucketServer.create` for the provider factory. + +The `@backstage/plugin-auth-backend` plugin also comes with a built-in resolver that can be used if desired. +The `emailMatchingUserEntityProfileEmail` identifies users by matching their Bitbucket Server email address to the email +address 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 with this resolver. diff --git a/docs/auth/google/gcp-iap-auth.md b/docs/auth/google/gcp-iap-auth.md index d6303ced6f..36eece85c5 100644 --- a/docs/auth/google/gcp-iap-auth.md +++ b/docs/auth/google/gcp-iap-auth.md @@ -29,7 +29,9 @@ auth: jwtHeader: x-custom-header # Optional: Only if you are using a custom header for the IAP JWT ``` -You can find the project number and service ID in the Google Cloud Console. +The full `audience` value can be obtained by visiting your [Identity-Aware Proxy Google Cloud console](https://console.cloud.google.com/security/iap), selecting your project, finding your Backend Service to proxy, clicking the 3 vertical dots then "Get JWT Audience Code", and copying from the resulting popup, which will look similar to the following: + +![Identity-Aware Proxy JWT Audience Code popup](../../assets/auth/gcp-iap-jwt-audience-code-popup.png) This config section must be in place for the provider to load at all. Now let's add the provider itself. @@ -41,7 +43,7 @@ besides the config section above, it also needs to be given one or more callbacks in actual code as well as described below. Add a `providerFactories` entry to the router in -`packages/backend/plugin/auth.ts`. +`packages/backend/src/plugins/auth.ts`. ```ts import { providers } from '@backstage/plugin-auth-backend'; @@ -75,7 +77,10 @@ export default async function createPlugin( // GSuite or similar, based on the IAP token sub/email claims const id = iapToken.email.split('@')[0]; const sub = stringifyEntityRef({ kind: 'User', name: id }); - const ent = [sub, stringifyEntityRef({ kind: 'Group', name: 'team-name' }); + const ent = [ + sub, + stringifyEntityRef({ kind: 'Group', name: 'team-name' }), + ]; return ctx.issueToken({ claims: { sub, ent } }); }, }, diff --git a/docs/auth/index.md b/docs/auth/index.md index fa185c0c23..8e15830aa8 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -18,6 +18,7 @@ Backstage comes with many common authentication providers in the core library: - [Auth0](auth0/provider.md) - [Azure](microsoft/provider.md) - [Bitbucket](bitbucket/provider.md) +- [Bitbucket Server](bitbucketServer/provider.md) - [Cloudflare Access](cloudflare/access.md) - [GitHub](github/provider.md) - [GitLab](gitlab/provider.md) diff --git a/docs/auth/microsoft/azure-easyauth.md b/docs/auth/microsoft/azure-easyauth.md new file mode 100644 index 0000000000..3571a3e5c5 --- /dev/null +++ b/docs/auth/microsoft/azure-easyauth.md @@ -0,0 +1,120 @@ +--- +id: azure-easy-auth +title: Azure EasyAuth Provider +sidebar_label: Azure EasyAuth +description: Adding Azure's EasyAuth Proxy as an authentication provider in Backstage +--- + +## Backstage Changes + +Add the following into your `app-config.yaml` or `app-config.production.yaml` file + +Add a `providerFactories` entry to the router in +`packages/backend/src/plugins/auth.ts`. + +```ts +import { providers } from '@backstage/plugin-auth-backend'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise<Router> { + const authProviderFactories = { + 'azure-easyauth': providers.easyAuth.create({ + signIn: { + resolver: async (info, ctx) => { + const { + fullProfile: { id }, + } = info.result; + + if (!id) { + throw new Error('User profile contained no id'); + } + + return await ctx.signInWithCatalogUser({ + annotations: { + 'graph.microsoft.com/user-id': id, + }, + }); + }, + }, + }), + }; + + return await createRouter({ + logger: env.logger, + config: env.config, + database: env.database, + discovery: env.discovery, + tokenManager: env.tokenManager, + providerFactories: authProviderFactories, + }); +} +``` + +Now the backend is ready to serve auth requests on the +`/api/auth/azure-easyauth/refresh` endpoint. All that's left is to update the frontend +sign-in mechanism to poll that endpoint through the IAP, on the user's behalf. + +## Frontend Changes + +It is recommended to use the `ProxiedSignInPage` for this provider, which is +installed in `packages/app/src/App.tsx` like this: + +```diff ++import { ProxiedSignInPage } from '@backstage/core-components'; + + const app = createApp({ + components: { ++ SignInPage: props => <ProxiedSignInPage {...props} provider="azure-easyauth" />, +``` + +See the [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) section for more information. + +## Azure Configuration + +How to configure azure depends on the service you're enable AAD auth on the app service. + +### Azure App Services + +To use EasyAuth with App Services, turn on Active Directory authentication +You must also enable the token store. + +The following example shows how to do this via a bicep template: + +```bicep +resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { + name: 'MY-WEBAPP-NAME' + + resource authConfig 'config' = { + name: 'authsettingsV2' + properties: { + globalValidation: { + redirectToProvider: 'AzureActiveDirectory' + requireAuthentication: true + unauthenticatedClientAction: 'RedirectToLoginPage' + } + login: { + tokenStore: { + enabled: true + } + } + platform: { + enabled: true + } + identityProviders: { + azureActiveDirectory: { + enabled: true + login: { + loginParameters: [ 'domain_hint=MYCOMPANY.COM' ] + } + registration: { + clientId: 'CLIENT-ID' + clientSecretSettingName: 'CLIENT-SECRET-NAME' + openIdIssuer: 'https://sts.windows.net/${tenant().tenantId}/v2.0' + } + } + } + } + } +} +``` diff --git a/docs/backend-system/architecture/01-index.md b/docs/backend-system/architecture/01-index.md index 5e6fb60ee6..aff730b0d1 100644 --- a/docs/backend-system/architecture/01-index.md +++ b/docs/backend-system/architecture/01-index.md @@ -1,23 +1,27 @@ --- id: index title: Backend System Architecture -sidebar_label: System Architecture +sidebar_label: Overview # prettier-ignore description: The structure and architecture of the new Backend System and its component parts --- -# Overview +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + +## Building Blocks This section introduces the high-level building blocks upon which this new system is built. These are all concepts that exist in our current system in one way or another, but they have all been lifted up to be first class concerns in -the new system. +the new system. Regardless of whether you are setting up your own backstage +instance, developing plugins, or extending plugins with new features, it is +important to understand these concepts. -## Building Blocks +The diagram below provides an overview of the different building blocks, and the other blocks that each of them interact with. -This section introduces the high-level building blocks upon which this new system is built. These are all concepts that exist in our current system in one way or another, but they have all been lifted up to be first class concerns in the new system. +![backend system building blocks diagram](../../assets/backend-system/architecture-building-blocks.drawio.svg) -<!-- TODO: GRAPH GO HERE --> +> NOTE: These are all concepts that existed in our old backend system in one way or another, but they have now all been lifted up to be first class concerns. ### Backend @@ -33,7 +37,7 @@ Plugins provide the actual features, just like in our existing system. They oper Services provide utilities to help make it simpler to implement plugins, so that each plugin doesn't need to implement everything from scratch. There are both many built-in services, like the ones for logging, database access, and reading configuration, but you can also import third-party services, or create your own. -Services are also a customization point for individual backend installations. You can both override services with your own implementations, as well as make smaller customizations to existing services. +Services are also a customization point for individual backend installations. You can override services with your own implementations, as well as make smaller customizations to existing services. ### Extension Points @@ -41,7 +45,7 @@ Many plugins have ways in which you can extend them, for example entity provider Extension Points look a little bit like services, since you depended on them just like you would a service. A key difference is that extension points are registered and provided by plugins themselves, based on what customizations each individual plugin wants to expose. -Extension Points are also exported separately from the plugin instance itself, and a single plugin can also expose multiple different extension points at once. This makes it easier to evolve and deprecated individual Extension Points over time, rather than dealing with a single large API surface. +Extension Points are also exported separately from the plugin instance itself, and a single plugin can also expose multiple different extension points at once. This makes it easier to evolve and deprecate individual Extension Points over time, rather than dealing with a single large API surface. ### Modules @@ -49,22 +53,20 @@ Modules use the plugin Extension Points to add new features for plugins. They mi Each module may only extend a single plugin, and the module must be deployed together with that plugin in the same backend instance. Modules may however only communicate with their plugin through its registered extension points. -Just like plugins, modules also have access to services and can depend on their own service implementations. They will however share services with the plugin that they extend, there are no module-specific service implementations. +Just like plugins, modules also have access to services and can depend on their own service implementations. They will however share services with the plugin that they extend - there are no module-specific service implementations. ## Package structure A detailed explanation of the package architecture can be found in the [Backstage Architecture Overview](../../overview/architecture-overview.md#package-architecture). The -most important packages to consider for this system are `backend`, -`plugin-<pluginId>-backend`, `plugin-<pluginId>-node`, and -`plugin-<pluginId>-backend-module-<moduleId>`. +most important packages to consider for this system are the following: - `plugin-<pluginId>-backend` houses the implementation of the backend plugins themselves. -- `plugin-<pluginId>-node` houses the extension points and any other utilities +- `plugin-<pluginId>-node` houses the backend plugin's extension points and any other utilities that modules or other plugins might need. - `plugin-<pluginId>-backend-module-<moduleId>` houses the modules that extend - the plugins via the extension points. + the plugin via its extension points. - `backend` is the backend itself that wires everything together to something that you can deploy. diff --git a/docs/backend-system/architecture/02-backends.md b/docs/backend-system/architecture/02-backends.md index c73a899863..b6079d71b4 100644 --- a/docs/backend-system/architecture/02-backends.md +++ b/docs/backend-system/architecture/02-backends.md @@ -3,12 +3,17 @@ id: backends title: Backend Instances sidebar_label: Backend # prettier-ignore -description: Service APIs for backend plugins +description: Backend instances --- -The new Backstage backend system is being built to help make it simpler to install backend plugins and to keep projects up to date. It also changes the foundation to one that makes it a lot easier to evolve plugins and the system itself with minimal disruption or cause for breaking changes. You can read more about the reasoning in the [original RFC](https://github.com/backstage/backstage/issues/11611). +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** -One of the goals of the new system was to reduce the code needed for setting up a Backstage backend and installing plugins. This is an example of how you create, add features, and start up your backend in the new system: +## The Backend Instance + +This is the main entry point for creating a backend. It does not have any functionality in and of itself, but is simply responsible for wiring things together. +It is up to you to decide how many different backends you want to deploy. You can have all features in a single one, or split things out into multiple smaller deployments. + +Below is a simple example of a backend that installs only the catalog plugin and starts it up. ```ts import { createBackend } from '@backstage/backend-defaults'; @@ -24,4 +29,12 @@ backend.add(catalogPlugin()); await backend.start(); ``` -One notable change that helped achieve this much slimmer backend setup is the introduction of a system for dependency injection, which is very similar to the one in the Backstage frontend. +`createBackend` is responsible for creating your backend instance, and wiring up all the services that you have provided. It deals with creating default implementations of all the [core services](../core-services/01-index.md) that are used by the plugins, and also provides a way to override the default implementations with your own. You can read more about creating services and overriding them in the [building backends docs](../building-backends/01-index.md). + +The backend instance has the ability to add features to the backend which are done using the `.add` method. Features are either plugins or modules, and you can read more about them in the [building plugins and modules docs](../building-plugins-and-modules/01-index.md). By default, a backend instance has no default features, and the services are responsible for wiring everything together. + +At a high level, when you call `createBackend`, it will create a new backend instance, which has a registry of all the services that are currently registered, and by adding features to the backend instance and calling the `.start()` method it will ensure that all the dependencies are wired up correctly and the `registerInit` methods are called in the correct order. + +Underneath the hood, `createBackend` calls `createSpecializedBackend` from `@backstage/backend-app-api` which is responsible for actually creating the backend instance, but with no services or no features. You can think of `createBackend` more of a 'batteries included' approach, and `createSpecializedBackend` a little more low level. + +As mentioned previously there's also the ability to create multiple of these backends in your project so that you can split apart your backend and deploy different backends that can scale independently of each other. For instance you might choose to deploy a backend with only the catalog plugin enabled, and one with just the scaffolder plugin enabled. We've provided some tools to be able to share services and defaults across your backend system, and you can find out more about that in the [shared environments docs](../building-backends/01-index.md#shared-environments). diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 27fea27db9..8a86d90efb 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -1,103 +1,229 @@ --- id: services -title: Backend Service APIs -sidebar_label: Service APIs +title: Backend Services +sidebar_label: Services # prettier-ignore -description: Service APIs for backend plugins +description: Services for backend plugins --- -## Backend Services +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** -The default backend provides several [core services](https://github.com/backstage/backstage/blob/master/packages/backend-plugin-api/src/services/definitions/coreServices.ts) out of the box which includes access to configuration, logging, databases and more. -Service dependencies are declared using their `ServiceRef`s in the `deps` section of the plugin or module, and the implementations are then forwarded to the `init` method of the plugin or module. +Backend services provide shared functionality available to all backend plugins and modules. They are made available through service references that embed a type that represents the service interface, similar to how [Utility APIs](../../api/utility-apis.md) work in the Backstage frontend system. To use a service in your plugin or module you request an implementation of that service using the service reference. -### Service References +The system surrounding services exists to provide a level of indirection between the service interfaces and their implementation. It is an implementation of dependency injection, where each backend instance is the dependency injection container. The implementation for each service is provided by a service factory, which encapsulates the logic for how each service instance is created. -A `ServiceRef` is a named reference to an interface which are later used to resolve the concrete service implementation. Conceptually this is very similar to `ApiRef`s in the frontend. -Services is what provides common utilities that previously resided in the `PluginEnvironment` such as Config, Logging and Database. +## Service Interfaces -On startup the backend will make sure that the services are initialized before being passed to the plugin/module that depend on them. -ServiceRefs contain a scope which is used to determine if the serviceFactory creating the service will create a new instance scoped per plugin/module or if it will be shared. `plugin` scoped services will be created once per plugin/module and `root` scoped services will be created once per backend instance. +Service interfaces can be any TypeScript type, but it is best to make it an object interface with a number of methods. General guidelines for interface design apply: keep them simple and lean, with few but powerful methods. Take care to avoid locking down the ways in which individual methods can evolve. Often you want to stick to a method with an options object as its only parameter, and return a result object. If there is any reason for uncertainty about whether the method should be async or not, always make it async. For example, a minimal interface should often use the following pattern: -#### Defining a Service +```ts +export interface FooService { + foo(options: FooOptions): Promise<FooResult>; +} +``` + +## Service References + +Once you have defined a service interface, you need to create a service reference using the `createServiceRef` function. This will create a `ServiceRef` instance, which is a reference that you export in order to allow users to interact with your service. Conceptually this is very similar to `ApiRef`s in the frontend system. For example: + +```ts +import { createServiceRef } from '@backstage/backend-plugin-api'; + +export interface FooService { + foo(options: FooOptions): Promise<FooResult>; +} + +export const fooServiceRef = createServiceRef<FooService>({ + id: 'example.foo', // the owner of this service is in this case the 'example' plugin +}); +``` + +The `fooServiceRef` that we create above should be exported, and can then be used to declare a dependency on the `FooService` interface and receive an implementation of it at runtime. + +When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'<pluginId>.<serviceName>'`. For more naming patters surrounding services, see the [naming patterns](./07-naming-patterns.md#services) page. + +A note on naming: the frontend and backend systems intentionally use the separate names "APIs" and "Services" for concepts that are quite similar. This is to avoid confusion between the two, both in documentation and discussion, but also in code. While the two systems are quite similar, they are not identical, and they can't be used interchangeably. + +## Service Factories + +In order to be able to depend on a service interface through a service reference, we of course also need to have some way of creating the concrete implementation of it. To encapsulate that logic we use service factories, which define both how service instances are created, as well as what other services they depend on for their implementation. + +Service factories can come from many different sources. There are built-in service factories, external ones that you can import from other packages, and you can also create your own. Specific service factories are installed within each backend instance, which acts as the dependency injection container. For any given backend instance there can only be a single designated service factory for each service. + +To define a service factory, we use `createServiceFactory`: + +```ts +import { createServiceFactory } from '@backstage/backend-plugin-api'; + +class DefaultFooService implements FooService { + async foo(options: FooOptions): Promise<FooResult> { + // ... + } +} + +export const fooServiceFactory = createServiceFactory({ + service: fooServiceRef, + deps: { bar: barServiceRef }, + factory({ bar }) { + return new DefaultFooService(bar); + }, +}); +``` + +To create a service factory we need to provide a reference to the `service` for which the factory will create instances, a `deps` object which lists the other services that the factory depends on, and a `factory` function which will be called to create the service instance. The backend system will call the `factory` function with an object that contains the service instances for each of the dependencies listed in the `deps` object. If a service implementation does not depend on any other services, the `deps` are left as an empty object (`{}`). The `factory` function must return a value that implements the service interface. + +If you need the creation of the service instance to be asynchronous, you can make the `factory` function async. For example: + +```ts +export const fooServiceFactory = createServiceFactory({ + service: fooServiceRef, + deps: {}, + async factory() { + const foo = new DefaultFooService(); + await foo.init(); + return foo; + }, +}); +``` + +Note that circular dependencies among service factories are not allowed. This is verified at runtime, and your backend instance will refuse to start up if it detects any conflicts. Likewise, the backend will also fail to start up if a service factory depends on a service that is not provided by any registered service factory. + +## Service Factory Options + +To install a service factory in a backend instance, we pass it in through the `services` option to `createBackend`: + +```ts +const backend = createBackend({ + services: [fooServiceFactory()], +}); +``` + +Note that we call `fooServiceFactory` to create the service factory instance. This is because `createServiceFactory` always returns a factory function that creates the actual service factory. This is done to always allow for options to be added to the service factory in the future, without breaking existing code. To add options to your service factory, you wrap the object passed to `createServiceFactory` in a callback that accepts the desired options. For example: + +```ts +export interface FooFactoryOptions { + mode: 'eager' | 'lazy'; +} + +export const fooServiceFactory = createServiceFactory( + (options?: FooFactoryOptions) => ({ + service: fooServiceRef, + deps: { bar: barServiceRef }, + factory({ bar }) { + return new DefaultFooService(bar, options?.mode); + }, + }), +); +``` + +This lets us use the options to customize the factory implementation in any way we want. From the outside the service factory looks just like before, except that we're now also able to pass options when installing the factory: + +```ts +const backend = createBackend({ + services: [fooServiceFactory({ mode: 'eager' })], +}); +``` + +## Core Services + +The backend system provides a number of core service definitions that both help implement the main functionality of the backend, but also provide a set of utilities for common concerns, such as logging, database access, job scheduling, and so on. These core services will always be present in a backend instance created with `createBackend`, and they can all be overridden with custom implementations if needed. + +The service references for all core services are exported via their own `coreServices` object, available from the `@backstage/backend-plugin-api` package. For example, the logging service is accessible via `coreServices.logger`. + +You can read more about what core services there are and how to use them in the [core services](../core-services/01-index.md) section. + +## Service Scope + +By default services are scoped to individual plugins, meaning that separate instances of the service will be created for each plugin. For example, in our `fooFactory` above, a separate instance of `DefaultFooService` will be created for every plugin that depends on the service. This both makes it possible to tailor the service implementations for the individual plugins, and also ensures some level of separation between plugins. + +The service scope is defined during the call to `createServiceRef`, with plugin scope being the default. Our above definition of the `fooServiceRef` is therefore equivalent to the following: + +```ts +export const fooServiceRef = createServiceRef<FooService>({ + scope: 'plugin', + id: 'example.foo', +}); +``` + +There are only two possible scopes for services, `'plugin'` and `'root'`. + +## Root Scoped Services + +If a service is defined as a root scoped service, the implementation created by the factory will be shared across all plugins and services. One other differentiating factory for root scoped services is that they are always initialized, regardless of whether any plugins depend on them or not. This makes them suitable for implementing backend-wide concerns that are not specific to any individual plugin. + +There is a limitation in the usage of root scoped services, which is that their implementation can only depend on other root scoped services. Plugin scoped services on the other hand can depend on both root and plugin scoped services. Because of this limitation, one of the main reasons to define a root scoped services is to make it possible for other root scoped services to depend on it. + +Because of these limitations and particular use-cases for root scoped services, they tend to be more rare than plugin scoped services. In general, you should prefer defining a service as plugin scoped, unless you are implementing either of the two mentioned use-cases. + +Some services come in pairs of a plugin and a root scoped service definition. For example, the `rootLogger` service is a root scoped service, while the `logger` service is a plugin scoped service. The `rootLogger` service houses the main logging implementation, while the `logger` service simply builds upon the `rootLogger` to add plugin specific labels. This division exists so that other root scoped services also have access to a logging service, but it is always preferable if the split can be avoided. If you do end up implementing this pattern, the root scoped service should be prefixed with `root`, this is to encourage use of the plugin scoped service instead. + +## Plugin Metadata + +Plugin scoped services have access to a plugin metadata service, which is a special service provided by the backend system that is not possible to override. The plugin metadata service provides information about the plugin that a service instance is being created for. It is itself a plugin scoped service, and can be depended on like any other service through the `coreServices.pluginMetadata` reference. + +The plugin metadata service is the base for all plugin specific customizations for services. For example, the default implementation of the plugin scoped logger service uses the plugin metadata service to attach the plugin ID as a field in all log messages: + +```ts +export const loggerServiceFactory = createServiceFactory({ + service: coreServices.logger, + deps: { + rootLogger: coreServices.rootLogger, + pluginMetadata: coreServices.pluginMetadata, + }, + factory({ rootLogger, pluginMetadata }) { + return rootLogger.child({ plugin: pluginMetadata.getId() }); + }, +}); +``` + +## Root Context for Service Factories + +Some services may benefit from having a context that is shared across all instances of a service. This of course only applies to plugin scoped services, as root scoped services only ever have a single instance. The root context could for example be used for sharing a common connection pool for database access, generated secrets for development, or any other kind of shared facility. Note that you should not use this to share state between plugins in production, as that would be a violation of the [plugin isolation rule](./04-plugins.md#rules-of-plugins). + +The root context is defined as part of the service factory by passing the `createRootContext` option: + +```ts +export const fooServiceFactory = createServiceFactory({ + service: fooServiceRef, + deps: { rootLogger: coreServices.rootLogger, bar: barServiceRef }, + createRootContext({ rootLogger }) { + return new FooRootContext(rootLogger); + } + factory({ bar }, ctx) { + return ctx.forPlugin(bar) + }, +}); +``` + +Whatever value is returned by the `createRootContext` function will shared and passed as the second argument to each invocation of the `factory` function. That way you can create a shared context that is used in the creation of each plugin instance. Unlike the `factory` function, the `createRootContext` function will only receive root scoped services as its dependencies, but just like the `factory` function, it can also be `async`. + +## Default Service Factories + +There are a lot of services that are installed in any standard Backstage backend instance by default. You can expect these services to always exist, and do not need to take any additional steps to make them available. This is not necessarily true for services that you import from external packages, as the user of your plugin or module might not have installed a factory for that service in their backend. In order to avoid having to ask integrators of your plugin to install a service factory for a service that you depend on, it is possible to define a default factory for a service. + +Default service factories are defined as part of the service reference by passing the `defaultFactory` option to `createServiceRef`: ```ts import { createServiceFactory, - coreServices, + createServiceRef, } from '@backstage/backend-plugin-api'; -import { ExampleImpl } from './ExampleImpl'; -export interface ExampleApi { - doSomething(): Promise<void>; -} - -export const exampleServiceRef = createServiceRef<ExampleApi>({ - id: 'example', - scope: 'plugin', // can be 'root' or 'plugin' - - // The defaultFactory is optional to implement but it will be used if no - // other factory is provided to the backend. This allows for the backend - // to provide a default implementation of the service without having to wire - // it beforehand. +export const fooServiceRef = createServiceRef<FooService>({ + id: 'example.foo', defaultFactory: async service => createServiceFactory({ service, - deps: { - logger: coreServices.logger, - plugin: coreServices.pluginMetadata, - }, - // This root context method is only available for plugin scoped services. - // It's only called once per backend instance. - // Logger is available at the root context level as it's also a root - // scoped service. - createRootContext({ logger }) { - return new ExampleImplFactory({ logger }); - }, - // Plugin is available as it's a plugin scoped service and will be - // created once per plugin. The logger can be had here too if needed. - // Both this anc the root context can also be async. - factory({ logger, plugin }, rootContext) { - // This block will be executed once for every plugin that depends on - // this service - logger.info('Initializing example service plugin instance'); - return rootContext.forPlugin(plugin.getId()); + deps: {}, + factory() { + return new DefaultFooService(); }, }), }); ``` -### Overriding Services +Note that we don't use the `fooServiceRef` when creating our service factory, but instead use the `service` parameter in the default factory callback. This is because attempting to use `fooServiceRef` directly would result in a circular reference. -In this example we replace the default root logger service implementation with a custom one that streams logs to GCP. The `rootLoggerServiceRef` has a `'root'` scope, meaning there are no plugin-specific instances of this service. +If a service defines a default factory, that factory will be used if there is no explicit factory registered in the backend for that service. This allows users of your service to directly import and use a service, without worrying about whether it is installed or not. It is recommended to always define a default factory for any service that you are exporting for use in other plugins or modules. -```ts -import { - createServiceFactory, - rootLoggerServiceRef, - LoggerService, -} from '@backstage/backend-plugin-api'; - -// This custom implementation would typically live separately from -// the backend setup code, either nearby such as in -// packages/backend/src/services/logger/GoogleCloudLogger.ts -// Or you can let it live in its own library package. -class GoogleCloudLogger implements LoggerService { - static factory = createServiceFactory({ - service: rootLoggerServiceRef, - deps: {}, - factory() { - return new GoogleCloudLogger(); - }, - }); - // custom implementation here ... -} - -// packages/backend/src/index.ts -const backend = createBackend({ - services: [ - // supplies additional or replacement services to the backend - GoogleCloudLogger.factory(), - ], -}); -``` +When defining a default factory for a service, it is possible for it to end up with duplicate implementations at runtime. This applies both to any shared root context in your factory, as well as plugin specific instances of your service. This is because package dependency version ranges may not line up perfectly, causing duplicate installations of the same package. This can happen both for two different plugins using the same service, but also across a plugin and its modules. If your service would break in this scenario, you should not define a default factory for it, but instead require that users of your service explicitly install a factory in their backend instance. diff --git a/docs/backend-system/architecture/04-plugins.md b/docs/backend-system/architecture/04-plugins.md index 6f077a0d2d..04271b17da 100644 --- a/docs/backend-system/architecture/04-plugins.md +++ b/docs/backend-system/architecture/04-plugins.md @@ -6,29 +6,28 @@ sidebar_label: Plugins description: Backend plugins --- -## Creating Plugins +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** -Plugins are created using the `createBackendPlugin` function. All plugins must have an ID and a register method. Plugins may also accept an options object, which can be either optional or required. The options are passed to the second parameter of the register method, and the options type is inferred and forwarded to the returned plugin factory function. +Plugins provide the actual base features of a Backstage backend. Each plugin operates completely independently of all other plugins and they only communicate with each other through network calls. This means that there is a strong degree of isolation between plugins, and that each plugin can be considered a separate microservice. While a default Backstage project has all plugins installed within a single backend, it is also possible to split this setup into multiple backends, with each backend housing one or more plugins. + +## Defining a Plugin + +Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./07-naming-patterns.md) about proper naming patterns. ```ts +// plugins/example-backend/src/plugin.ts import { - configServiceRef, coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -// export type ExamplePluginOptions = { exampleOption: boolean }; export const examplePlugin = createBackendPlugin({ - // unique id for the plugin - id: 'example', - // It's possible to provide options to the plugin - // register(env, options: ExamplePluginOptions) { + pluginId: 'example', register(env) { env.registerInit({ deps: { logger: coreServices.logger, }, - // logger is provided by the backend based on the dependency on loggerServiceRef above. async init({ logger }) { logger.info('Hello from example plugin'); }, @@ -37,32 +36,60 @@ export const examplePlugin = createBackendPlugin({ }); ``` -The plugin can then be installed in the backend using the returned plugin factory function: +The `env` object passed to the `register` callback contains different methods that declare the external surface of the plugin. The `env.registerInit` method is used to register an initialization function that is run when the backend starts up. The `deps` argument is used to declare service dependencies, and the `init` callback is passed an object with the resolved dependencies. In this case, we declare a dependency on the logger service, which is one of the core services available to all Backstage backend plugins. For a full list of core services as well as documentation for each services, see the [core services section](../core-services/01-index.md). Plugins can of course also depend on services exported by other libraries. + +The `createBackendPlugin` return value is exported as `examplePlugin`, which is a factory function used to create the actual plugin instance. For example, to install the plugin in your backend instance, you would do the following: ```ts backend.add(examplePlugin()); ``` -If we wanted our plugin to accept options as well, we'd accept the options as the second parameter of the register method: +The reason for why our plugin instance has been wrapped up in a factory function is so that you can always chose to add options to your plugin in the future, without breaking existing usage. To add options you wrap the object passed to `createBackendPlugin` in a callback that accepts the desired options. For example: ```ts -export const examplePlugin = createBackendPlugin({ - id: 'example', - register(env, options?: { silent?: boolean }) { - env.registerInit({ - deps: { logger: coreServices.logger }, - async init({ logger }) { - if (!options?.silent) { - logger.info('Hello from example plugin'); - } - }, - }); - }, -}); +export interface ExamplePluginOptions { + skipHello: boolean; +} + +export const examplePlugin = createBackendPlugin( + (options?: ExamplePluginOptions) => ({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + logger: coreServices.logger, + }, + async init({ logger }) { + if (!options?.skipHello) { + logger.info('Hello from example plugin'); + } + }, + }); + }, + }), +); ``` -Passing the option to the plugin during installation looks like this: +Now your plugin accepts an optional options object, which can be used to configure each plugin instance. To supply options to the plugin, you pass them to the plugin factory method: ```ts -backend.add(examplePlugin({ silent: true })); +backend.add(examplePlugin({ skipHello: true })); ``` + +It is also possible to make the options required, simply remove the `?` from the parameter declaration. This will be reflected in the returned factory function, which will now require the options parameter. + +Options are a simple way to allow for more lightweight customization of a plugin, but they do not allow for more complex extensions that require access to services. For that, you need to create and register extension points for your plugin, which are covered in the [next section](./05-extension-points.md). + +## Rules of Plugins + +The following rules apply to the production setup of Backstage plugins in the broader Backstage plugin ecosystem. Any plugin that is maintained under the `@backstage` package namespace should follow these rules, and it is recommended that all widely distributed plugins follow these rules as well. + +An exception to these rules are made for development or test setups, where shortcuts can be take in order to streamline development and keep things simple. + +### Scalable + +Plugins must always be designed to be horizontally scalable. This means that you should not keep any state in memory, or make sure that replicating this state across multiple instances is not an issue. Plugins should either be stateless, or store their state in an external service, such as a database. + +### Isolated + +Plugins must never communicate with each other directly through code, they may only communicate over the network. Plugins that wish to expose an external interface for other plugins and modules to use are recommended to do so though a [node-library](../../local-dev/cli-build-system.md#package-roles) package. The library should export an API client service to make calls to your plugin, or similar construct. diff --git a/docs/backend-system/architecture/05-extension-points.md b/docs/backend-system/architecture/05-extension-points.md index d74a4690c9..6d320037ba 100644 --- a/docs/backend-system/architecture/05-extension-points.md +++ b/docs/backend-system/architecture/05-extension-points.md @@ -6,11 +6,17 @@ sidebar_label: Extension Points description: Extension points of backend plugins --- -### Extension Points +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** -Modules depend on extension points just as a regular dependency by specifying it in the `deps` section. +While plugins are able to accept options for lightweight forms of customization and extension, you quickly hit a limit where you need something more powerful to allow users to extend your plugin. For this purpose, the backend system provides a mechanism for plugins to provide extension points, which can be used to expose deeper customizations for your plugin. Extension points are used by modules, which are installed in the backend adjacent to plugins. Modules are covered more in-depth in the [next section](./06-modules.md). -#### Defining an Extension Point +Extension points are quite similar to services, in that they both encapsulate an interface in a reference object. The key difference is that extension points are registered and provided by plugins themselves, and do not have any factory associated with them. Extension points for a given plugin are also only accessible to modules that extend that same plugin. + +Extension points should always be exported from a plugin node library package, for example `@backstage/plugin-catalog-node`. This is to allow for modules to avoid a direct dependency on the plugin, and make it easier to evolve extension points over time. You can export as many different extension points as you want, just be mindful of the complexity of the API surface. It is however often better to export multiple extension points with few methods, rather than few extension points with many methods, as that tends to be easier to maintain. + +## Defining an Extension Point + +Extension points are created using the `createExtensionPoint` method from `@backstage/backend-plugin-api`. You need to provide the type, as well as an ID. ```ts import { createExtensionPoint } from '@backstage/backend-plugin-api'; @@ -25,6 +31,54 @@ export const scaffolderActionsExtensionPoint = }); ``` -#### Registering an Extension Point +## Registering an Extension Point -Extension points are registered by a plugin and extended by modules. +For modules to be able to use your extension point, an implementation of it must be registered by the plugin. This is done using the `registerExtensionPoint` method in the `register` callback of the plugin definition. + +```ts +class ActionsExtension implements ScaffolderActionsExtensionPoint { + addActions(...actions: TemplateAction<any>[]): void { ... } + + getRegisteredActions() { ... } +} + +export const scaffolderPlugin = createBackendPlugin( + { + pluginId: 'scaffolder', + register(env) { + const actionsExtensions = new ActionsExtension(); + env.registerExtensionPoint( + scaffolderActionsExtensionPoint, + actionsExtensions, + ); + + env.registerInit({ + deps: { ... }, + async init({ ... }) { + const actions = actionsExtension.getRegisteredActions(); + + // Use the registered actions when setting up the scaffolder ... + }, + }); + }, + }, +); +``` + +There are a couple of things to note here. The first is that our `ActionsExtension` class both implements the `ScaffolderActionsExtensionPoint` interface, but also has additional public methods. These methods won't be available to the modules that use this extension, but we can use them here in the implementation of our plugin. Note also that the `ActionsExtension` class is _not_ exported to the outside, since it's only for the internal use of this plugin during its setup phase. + +The second is that we create our `ActionsExtension` instance within the `register` method, and then access it directly in our `init` method. This is both safe to do and an intended convenience. All modules that extend our plugin will be completely initialized before our plugin gets initialized, which means that at the point where our `init` method is called, all actions have been added and can be accessed. + +## Extension Point Design + +Designing the extension point interface requires careful consideration. It is a public API surface that your plugin will need to maintain over time. Keep in mind that the installation of modules is an intentional action by the user, meaning you can always design an extension point interface for additions only. For example, there is no need for the `scaffolderActionsExtensionPoint` to support removal of actions, since the user can just uninstall the module that added the action. + +Another pattern that can be used is a type of singleton pattern where the extension point is used to add or override some default behavior. For example, let's say the scaffolder wants to expose a way to customize the execution of template tasks. It wouldn't really make sense to allow multiple modules to each add their own task runners, so instead we use a setter to ensure that only one task runner is installed, throwing an error if any other module tries to install another task runner. + +```ts +interface ScaffolderTaskRunnerExtensionPoint { + setTaskRunner(taskRunner: TaskRunner): void; +} +``` + +If you want to make breaking changes to an extension point that already has some usage, we recommend that you instead deprecate the existing one and create a new one with a different name. You might want to use a completely new name, but you can also suffix the existing one with a version number, for example `scaffolderActionsV2ExtensionPoint`. diff --git a/docs/backend-system/architecture/06-modules.md b/docs/backend-system/architecture/06-modules.md index 428e51fafa..b4a2e8338a 100644 --- a/docs/backend-system/architecture/06-modules.md +++ b/docs/backend-system/architecture/06-modules.md @@ -6,35 +6,40 @@ sidebar_label: Modules description: Modules for backend plugins --- -## Creating Modules +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** -Some facts about modules +Backend modules are used to extend [plugins](./04-plugins.md) with additional features or change existing behavior. They must always be installed in the same backend instance as the plugin that they extend, and may only extend a single plugin. Modules interact with their target plugin using the [extension points](./05-extension-points.md) registered by the plugin, while also being able to depend on the [services](./03-services.md) of that plugin. -- A Module is able to extend a plugin with additional functionality using the `ExtensionPoint`s registered by the plugin. -- A module can only extend one plugin but can interact with multiple `ExtensionPoint`s registered by that plugin. -- A module is always initialized before the plugin it extends. +Both modules and plugins register an `init` method that is called during startup. In order to ensure that modules have registered all their extensions before the plugin starts up, all modules for each plugin are completely initialized before the plugin itself is initialized. In practice this means that all promises returned by each `init` method of the modules need to resolve before the plugin `init` method is called. This also means that it is not possible to further interact with the extension points once the `init` method has resolved. -A module depends on the `ExtensionPoint`s exported by the target plugin's library package, for example `@backstage/plugin-catalog-node`, and does not directly declare a dependency on the plugin package itself. +A module depends on the extension points exported by the target plugin's library package, for example `@backstage/plugin-catalog-node`, and does not directly declare a dependency on the plugin package itself. This is to avoid a direct dependency and potentially cause duplicate installations of the plugin package, while duplicate installations of library packages should always be supported. -Here's an example on how to create a module that adds a new processor using the `catalogProcessingExtensionPoint`: +## A Practical Example + +The following is an example on how to create a module that adds a new processor using the `catalogProcessingExtensionPoint`: ```ts import { createBackendModule } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; -import { MyCustomProcessor } from './processor'; +import { MyCustomProcessor } from './MyCustomProcessor'; -export const exampleCustomProcessorCatalogModule = createBackendModule({ +export const catalogModuleExampleCustomProcessor = createBackendModule({ moduleId: 'exampleCustomProcessor', pluginId: 'catalog', register(env) { env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, + logger: coreServices.logger, }, async init({ catalog }) { - catalog.addProcessor(new MyCustomProcessor()); + catalog.addProcessor(new MyCustomProcessor(logger)); }, }); }, }); ``` + +Notice that we're placing the extension point we want to interact with in the `deps` option, while also depending on the logger service at the same time. When initializing modules we can depend on both extension points and services interchangeably. You can also depend on multiple extension points at once, in case the implementation of the module requires it. + +It is typically best to keep modules slim and to each only add a single new feature. It is often the case that it is better to create two separate modules rather than one that provides both features. The one limitation here is that modules can not interact with each other and need to be self contained. diff --git a/docs/backend-system/architecture/07-naming-patterns.md b/docs/backend-system/architecture/07-naming-patterns.md new file mode 100644 index 0000000000..b541cc09ba --- /dev/null +++ b/docs/backend-system/architecture/07-naming-patterns.md @@ -0,0 +1,96 @@ +--- +id: naming-patterns +title: Backend System Naming Patterns +sidebar_label: Naming Patterns +# prettier-ignore +description: Naming patterns in the backend system +--- + +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + +These are the naming patterns to adhere to within the backend system. They help us keep exports consistent across packages and make it easier to understand the usage and intent of exports. + +### Plugins + +| Description | Pattern | Examples | +| ----------- | ------------ | ----------------------------------- | +| export | `<id>Plugin` | `catalogPlugin`, `scaffolderPlugin` | +| ID | `'<id>'` | `'catalog'`, `'scaffolder'` | + +Example: + +```ts +export const catalogPlugin = createBackendPlugin({ + pluginId: 'catalog', + ... +}) +``` + +### Modules + +| Description | Pattern | Examples | +| ----------- | ---------------------------- | ----------------------------------- | +| export | `<pluginId>Module<ModuleId>` | `catalogModuleGithubEntityProvider` | +| ID | `'<moduleId>'` | `'githubEntityProvider'` | + +Example: + +```ts +export const catalogModuleGithubEntityProvider = createBackendModule({ + pluginId: 'catalog', + moduleId: 'githubEntityProvider', + ... +}) +``` + +### Extensions + +| Description | Pattern | Examples | +| ----------- | -------------------------------- | -------------------------------------- | +| Interface | `<PluginId><Name>ExtensionPoint` | `CatalogProcessingExtensionPoint` | +| Reference | `<pluginId><Name>ExtensionPoint` | `catalogProcessingExtensionPoint` | +| ID | `'<pluginId>.<name>'` | `'catalog.processing'`, `'foo.barBaz'` | + +Example: + +```ts +export interface CatalogProcessingExtensionPoint { + ... +} + +export const catalogProcessingExtensionPoint = createExtensionPoint<CatalogProcessingExtensionPoint>({ + id: 'catalog.processing', + ... +}) +``` + +### Services + +| Description | Pattern | Examples | +| ----------- | ---------------------- | -------------------------------------------------- | +| Interface | `<Name>Service` | `LoggerService`, `DatabaseService` | +| Reference | `<name>ServiceRef` | `loggerServiceRef`, `databaseServiceRef` | +| ID | `<pluginId>.<name>` | `'core.rootHttpRouter'`, `'catalog.catalogClient'` | +| Factory | `<name>ServiceFactory` | `loggerServiceFactory`, `databaseServiceFactory` | + +Example: + +```ts +export interface CatalogClientService { + ... +} + +export const catalogClientServiceRef = createServiceRef<CatalogClientService>({ + id: 'catalog.catalogClient', + ... +}) + +export const catalogClientServiceFactory = createServiceFactory({ + service: catalogClientServiceRef, + ... +}) +``` + +An exception to the above service reference naming pattern has been made for the all of the core services in the core API. The `@backstage/backend-plugin-api` makes all core service references available via a single `coreServices` collection. Likewise, the `@backstage/backend-test-utils` exports all mock service implementations via a single `mockServices` collection. This means that the table above is slightly misleading, since `loggerServiceRef` and `databaseServiceRef` are instead available as `coreServices.logger` and `coreService.database`. We recommend that plugins avoid this patterns unless they have a very large number of services that they need to export. + +While it is often preferred to prefix root scoped services with `Root`, it is not required. For example, `RootHttpRouterService` and `RootLifecycleService` follow this pattern, but `ConfigService` doesn't and it is a root scoped service. diff --git a/docs/backend-system/building-backends/01-index.md b/docs/backend-system/building-backends/01-index.md index b9879f4b24..66bf68ab07 100644 --- a/docs/backend-system/building-backends/01-index.md +++ b/docs/backend-system/building-backends/01-index.md @@ -6,7 +6,177 @@ sidebar_label: Overview description: Building backends using the new backend system --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + > NOTE: If you have an existing backend that is not yet using the new backend > system, see [migrating](./08-migrating.md). +This section covers how to set up and customize your own Backstage backend. It covers some aspects of how backend instances fit into the larger system, but for a more in-depth explanation of the role of backends in the backend system, see [the architecture section](../architecture/02-backends.md). + # Overview + +A minimal Backstage backend is very lightweight. It is a single package with a `package.json` file and a `src/index.ts` file, not counting surrounding tooling and documentation. The package is typically placed within the `packages/backend` folder of a Backstage monorepo, but that is up to you. The backend package is part of any project created with `@backstage/create-app`, so you typically do not need to create it yourself. + +When you create a new project with `@backstage/create-app`, you'll get a backend package with a `src/index.ts` that looks something like this: + +```ts +import { createBackend } from '@backstage/backend-defaults'; +import { appPlugin } from '@backstage/plugin-app-backend'; +import { catalogPlugin } from '@backstage/plugin-catalog-backend'; +import { + scaffolderPlugin, + catalogModuleTemplateKind, +} from '@backstage/plugin-scaffolder-backend'; + +const backend = createBackend(); + +backend.add(appPlugin()); +backend.add(catalogPlugin()); +backend.add(catalogModuleTemplateKind()); +backend.add(scaffolderPlugin()); + +backend.start(); +``` + +There will be a couple more plugins and modules in the initial setup, but the overall layout is the same. + +What we're doing in this file is creating a new backend using `createBackend`, and then installing a collection of different plugins and modules that we want to be part of that backend. Plugins are standalone features, while modules augment existing plugins. Each module can only target a single plugin, and that plugin must also be present in the same backend. Finally, we start up the backend by calling the `start` method. + +## Customization + +Apart from installing existing plugins and modules in the backend, there are a couple of different ways in which you can customize your backend installation. + +### Configuration + +Perhaps the most accessible way is though static configuration, which you can read more about in the documentation for how to [write configuration](../../conf/writing.md). Many different aspects of the backend can be configured, including both the behavior of the backend itself, as well as many plugins or modules. You'll need to refer to the documentation of each plugin or module to see what configuration is available. Also be sure to check out the documentation of the [core services](../core-services/01-index.md), as that also covers how to configure those. + +### Services + +Speaking of services, they are another important point of customization. Services allow you to make deeper and broader customizations of the backend. They are similar to [Utility APIs](../../api/utility-apis.md) in the frontend system, using dependency injection to make common functionality available to plugins and modules. You can read more about services in the [architecture section](../architecture/03-services.md). + +There is a core set of services that must be installed in all backends, for things like logging, database access, serving HTTP, and so on. Luckily, all of these services are installed by default when you use `createBackend` from the `@backstage/backend-defaults` package, which is what you have in a standard setup. + +All of these services can be replaced with your own implementations if you need to customize them. The simplest way to do this is to use the existing service implementations but with additional options. Many of the core services can be customized this way, but not all, as they don't all have meaningful options. + +For example, let's say we want to customize the core configuration service to enable remote configuration loading. That would look something like this: + +```ts +import { configServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + configServiceFactory({ + remote: { reloadIntervalSeconds: 60 }, + }), + ], +}); +``` + +This will make it possible to pass URLs as configuration targets, and those URLs will be polled every 60 seconds for changes. + +There is one exception to the above, which is the built-in `PluginMetadataService` that is provided by the framework and is not possible to override. + +### Custom Service Implementations + +When overriding services you are not limited to the existing implementations, you can also provide your own custom service factories. This will let you globally override services with completely custom implementations, or build on existing implementations to add additional logic. + +To override a service, you provide it in the `services` option just like above, but this time we need to use `createServiceFactory` to create our factory. For example, if you want to replace the default `LoggerService` with your own, it might look like this: + +```ts +const backend = createBackend({ + services: [ + createServiceFactory({ + service: coreServices.logger, + deps: { + rootLogger: coreServices.rootLogger, + plugin: coreServices.pluginMetadata, + config: coreServices.config, + }, + factory({ rootLogger, plugin, config }) { + const labels = readCustomLogLabelsForPlugin(config, plugin); // custom logic + return rootLogger.child(labels); + }, + }), + ], +}); +``` + +The `LoggerService` is responsible for creating a specialized logger instance for each plugin, while the `RootLoggerService` is the actual logging implementation. The default implementation of `LoggerService` will decorate the logger with a `plugin` label that contains the plugin ID. Here in our custom implementation we read out additional labels from the configuration and add those as well. + +This example touches on the fact that services can have different scopes, being either scoped to individual plugins or the root backend instance. To read more about this, see the [architecture section](../architecture/03-services.md). + +## Split Into Multiple Backends + +> NOTE: Splitting into multiple backends is an advanced deployment pattern that requires significant effort and there are not yet many built-in tools in the framework to help you out. Only use this if necessary. + +A more advanced way to deploy Backstage is to split the backend plugins into multiple different backend deployments. Both the [deployment documentation](../../deployment/scaling.md) and [Threat Model](../../overview/threat-model.md#trust-model) explain the benefits of this, so here we'll focus on how to do it. + +To create a separate backend we need to create an additional backend package. This package will be built and deployed separately from your existing backend. There is currently no template to create a backend via `yarn new`, so the quickest way is to copy the new package and modify. The naming is up to you and it depends on how you are splitting things and up. For this example we'll just use a simple suffix. You might end up with a directory structure like this: + +```text +packages/ + backend-a/ + src/ + index.ts + package.json <- "name": "backend-a" + backend-b/ + src/ + index.ts + package.json <- "name": "backend-b" +``` + +You can now trim down the `src/index.ts` files to only include the plugins and modules that you want to be part of that backend. For example, if you want to split out the scaffolder plugin, you might end up with something like this: + +```ts +// packages/backend-a/src/index.ts, imports omitted +const backend = createBackend(); + +backend.add(appPlugin()); +backend.add(catalogPlugin()); +backend.add(catalogModuleTemplateKind()); +backend.start(); +``` + +And `backend-b`, don't forget to clean up dependencies in `package.json` as well: + +```ts +// packages/backend-b/src/index.ts, imports omitted +const backend = createBackend(); + +backend.add(scaffolderPlugin()); +backend.start(); +``` + +We've now split the backend into two separate deployments, but we still need to make sure that they can communicate with each other. This is the hard and somewhat tedious part, as Backstage currently doesn't provide an out of the box solution that solves this. You'll need to manually configure the two backends with custom implementations of the `DiscoveryService` and have them return the correct URLs for each other. Likewise, you'll also need to provide a custom implementation of the `DiscoveryApi` in the frontend, unless you surface the two backends via a proxy that handles the routing instead. + +### Shared Environments + +To make it a bit easier to manage multiple backends, it's possible to create a shared environment that can be used across multiple backends. You would typically house it in a separate package that can be referenced by backends in your monorepo, or published to a package registry for broader use. + +A shared environment contains a set of service implementations that should be used across all backends. These services will override the default ones, but if a service is provided directly to the backend, it will override the one in the shared environment. + +A shared environment is defined using `createSharedEnvironment`. In this example we place it in a new and separate package called `backend-env`: + +```ts +// packages/backend-env/src/index.ts +import { createSharedEnvironment } from '@backstage/backend-plugin-api'; +import { customDiscoveryServiceFactory } from './customDiscoveryServiceFactory'; + +export const env = createSharedEnvironment({ + services: [ + customDiscoveryServiceFactory(), // custom DiscoveryService implementation + ], +}); +``` + +And passed on to backends using the `env` option: + +```ts +// packages/backend-b/src/index.ts, imports omitted +import { env } from '@internal/backend-env'; + +const backend = createBackend({ env }); + +backend.add(scaffolderPlugin()); +backend.start(); +``` diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index 84f801f15d..567476532f 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -6,4 +6,428 @@ sidebar_label: Migration Guide description: How to migrate existing backends to the new backend system --- -# Overview +> **DISCLAIMER: The new backend system is in alpha, and still under active development. As such, it is not considered stable, and it is not recommended to migrate production backends to the new backend system until it has a stable release.** + +## Overview + +This section describes how to migrate an existing Backstage backend service +package (typically in `packages/backend`) to use the new backend system. + +One of the main benefits of the new backend system is that it abstracts away the +way that plugins and their dependencies are wired up, leading to a significantly +simplified backend package that rarely if ever needs to change when plugins or +their dependencies evolve. You generally don't have to convert all of your +internal plugins and support classes themselves to the backend system first - +the migration here will mostly deal with wiring and using compatibility wrappers +where possible in the backend package itself. We hope that you will find that +you end up with a much smaller, easier to understand, and easier to maintain +package as a result of these steps, and then being able to [migrate +plugins](../building-plugins-and-modules/08-migrating.md) as a separate +endeavour later. + +## Overall Structure + +Your typical backend package has a few overall component parts: + +- An `index.ts` file that houses all of the creation and wiring together of all + of the plugins and their dependencies +- A `types.ts` file that defines the "environment", i.e. the various + dependencies that get created by the backend and passed down into each plugin +- A `plugins` folder which has one file for each plugin, e.g. + `plugins/catalog.ts` + +The index file has this overall shape: + +```ts +import todo from './plugins/todo'; // repeated for N plugins + +function makeCreateEnv(config: Config) { + return (plugin: string): PluginEnvironment => { + // ... build per-plugin environment + }; +} + +async function main() { + // ... early init + const createEnv = makeCreateEnv(config); + const todoEnv = useHotMemoize(module, () => createEnv('todo')); // repeated for N plugins + const apiRouter = Router(); + apiRouter.use('/todo', await todo(todoEnv)); // repeated for N plugins + // ... wire up and start http server +} + +module.hot?.accept(); +main().catch(...); +``` + +## Migrating the Index File + +This migration will try to leave the `plugins` folder unchanged initially, first +focusing on removing the environment type and reducing the index file to its +bare minimum. Then as a later step, we can reduce the `plugins` folder bit by +bit, replacing those files generally with one-liners in the index file instead. + +Let's start by establishing the basis of your new index file. You may want to +comment out its old contents, or renaming the old file to `index.backup.ts` for +reference and making a new blank one to work on - whichever works best for you. +These are our new blank contents in the index file: + +```ts +// packages/backend/src/index.ts +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); +backend.start(); +``` + +Note that the environment builder and the `main` dance are entirely gone. + +We'll also want to add some backend system packages as dependencies. Run the +following command: + +```bash +# from the repository root +yarn add --cwd packages/backend @backstage/backend-defaults @backstage/backend-plugin-api +``` + +You should now be able to start this up with the familiar `yarn workspace +backend start` command locally and seeing some logs scroll by. But it'll just be +a blank service with no real features added. So let's stop it with `Ctrl+C` and +reintroduce some plugins into the mix. + +```diff + import { createBackend } from '@backstage/backend-defaults'; ++import { legacyPlugin } from '@backstage/backend-common'; + + const backend = createBackend(); ++backend.add(legacyPlugin('todo', import('./plugins/todo'))); + backend.start(); +``` + +The `todo` plugin used above is just an example and you may not have it enabled +in your own backend. Feel free to change it to some other plugin that you +actually have in your `plugins` folder, for example +`backend.add(legacyPlugin('catalog', import('./plugins/catalog')))`. + +The `legacyPlugin` helper makes it easy to bridge the gap between the old-style +plugin files and the new backend system. It ensures that the dependencies that +you used to have to declare by hand in your env are gathered behind the scenes, +then passes them into the relevant `createPlugin` export function, and makes +sure that the route handler it returns is passed into the HTTP router with the +given prefix. + +## Handling Custom Environments + +In the simple case, what we did above is sufficient, TypeScript is happy, and +the backend runs with the new feature. If they do, feel free to skip this entire +section, and deleting `types.ts`. + +Sometimes though, type errors can be reported on the newly added line, saying +that parts of the `PluginEnvironment` type do not match. This happens when the +environment was changed from the defaults, perhaps with your own custom +additions. If this is the case in your installation, you still aren't out of +luck - you can build a customized `legacyPlugin` function. + +```diff + import { createBackend } from '@backstage/backend-defaults'; +-import { legacyPlugin } from '@backstage/backend-common'; ++import { makeLegacyPlugin, loggerToWinstonLogger } from '@backstage/backend-common'; ++import { coreServices } from '@backstage/backend-plugin-api'; + ++const legacyPlugin = makeLegacyPlugin( ++ { ++ cache: coreServices.cache, ++ config: coreServices.config, ++ database: coreServices.database, ++ discovery: coreServices.discovery, ++ logger: coreServices.logger, ++ permissions: coreServices.permissions, ++ scheduler: coreServices.scheduler, ++ tokenManager: coreServices.tokenManager, ++ reader: coreServices.urlReader, ++ identity: coreServices.identity, ++ // ... and your own additions ++ }, ++ { ++ logger: log => loggerToWinstonLogger(log), ++ }, ++); + + const backend = createBackend(); + backend.add(legacyPlugin('todo', import('./plugins/todo'))); + backend.start(); +``` + +The first argument to `makeLegacyPlugin` is the mapping from environment keys to +references to actual [backend system services](../architecture/03-services.md). +The second argument allows you to "tweak" the types of those services to +something more fitting to your env. For example, you'll see that the logger +service API type was changed from the raw Winston logger of old, to a different, +custom API, so we use a helper function to transform that particular one. + +To make additions as mentioned above to the environment, you will start to get +into the weeds of how the backend system wiring works. You'll need to have a +service reference and a service factory that performs the actual creation of +your service. Please see [the services +article](../architecture/03-services.md#defining-a-service) to learn how to +create a service ref and its default factory. You can place that code directly +in the index file for now if you want, or near the actual implementation class +in question. + +In this example, we'll assume that your added environment field is named +`example`, and the created ref is named `exampleServiceRef`. + +```diff ++import { exampleServiceRef } from '<somewhere>'; // if the definition is elsewhere + + const legacyPlugin = makeLegacyPlugin( + { + // ... the above core services still go here ++ example: exampleServiceRef + }, + { + logger: log => loggerToWinstonLogger(log), + }, + ); +``` + +After this, your backend will know how to instantiate your thing on demand and +place it in the legacy plugin environment. + +> NOTE: If you happen to be dealing with a service ref that does NOT have a +> default implementation, but rather has a separate service factory, then you +> will also need to import that factory and pass it to the `services` array +> argument of `createBackend`. + +## Cleaning Up the Plugins Folder + +For plugins that are private and your own, you can follow a [dedicated migration +guide](../building-plugins-and-modules/08-migrating.md) as you see fit, at a +later time. + +For third party backend plugins, in particular the larger core plugins that are +maintained by the Backstage maintainers, you may find that they have already +been migrated to the new backend system. This section describes some specific +such migrations you can make. + +> NOTE: For each of these, note that your backend still needs to have a +> dependency (e.g. in `packages/backend/package.json`) to those plugin packages, +> and they still need to be configured properly in your app-config. Those +> mechanisms still work just the same as they used to in the old backend system. + +### The App Plugin + +The app backend plugin that serves the frontend from the backend can trivially +be used in its new form. + +```diff + // packages/backend/src/index.ts ++import { appPlugin } from '@backstage/plugin-app-backend'; + + const backend = createBackend(); ++backend.add(appPlugin({ appPackageName: 'app' })); +``` + +This is an example of how options can be passed into some backend plugins. The +app plugin specifically needs to know the name of the package that holds the +frontend code. This is the `"name"` field in that package's `package.json`, +typically found in your `packages/app` folder. By default it's just plain "app". + +You should be able to delete the `plugins/app.ts` file at this point. + +### The Catalog Plugin + +A basic installation of the catalog plugin looks as follows. + +```diff + // packages/backend/src/index.ts ++import { catalogPlugin } from '@backstage/plugin-catalog-backend'; ++import { catalogModuleTemplateKind } from '@backstage/plugin-scaffolder-backend'; + + const backend = createBackend(); ++backend.add(catalogPlugin()); ++backend.add(catalogModuleTemplateKind()); +``` + +Note that this also installs a module from the scaffolder, namely the one which +enables the use of the `Template` kind. In the unlikely event that you do not +use templates at all, you can remove those lines. + +If you have other customizations made to `plugins/catalog.ts`, such as adding +custom processors or entity providers, read on. Otherwise, you should be able to +just delete that file at this point. + +You will use the [extension points](../architecture/05-extension-points.md) +mechanism to extend or tweak the functionality of the plugin. To do that, +you'll make your own bespoke [module](../architecture/06-modules.md) which +depends on the appropriate extension point and interacts with it. + +```diff + // packages/backend/src/index.ts ++import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; ++import { createBackendModule } from '@backstage/backend-plugin-api'; + ++const catalogModuleCustomExtensions = createBackendModule({ ++ pluginId: 'catalog', // name of the plugin that the module is targeting ++ moduleId: 'customExtensions', ++ register(env) { ++ env.registerInit({ ++ deps: { ++ catalog: catalogProcessingExtensionPoint, ++ // ... and other dependencies as needed ++ }, ++ init({ catalog /* ..., other dependencies */ }) { ++ // Here you have the opportunity to interact with the extension ++ // point before the plugin itself gets instantiated ++ catalog.addEntityProvider(new MyEntityProvider()); // just an example ++ catalog.addProcessor(new MyProcessor()); // just an example ++ }, ++ }); ++ }, ++}); + + const backend = createBackend(); + backend.add(catalogPlugin()); + backend.add(catalogModuleTemplateKind()); ++backend.add(catalogModuleCustomExtensions()); +``` + +This also requires that you have a dependency on the corresponding node package, +if you didn't already have one. + +```bash +# from the repository root +yarn add --cwd packages/backend @backstage/plugin-catalog-node +``` + +Here we've placed the module directly in the backend index file just to get +going easily, but feel free to move it out to where it fits best. As you migrate +your entire plugin flora to the new backend system, you will probably make more +and more of these modules as "first class" things, living right next to the +implementations that they represent, and being exported from there. + +### The Events Plugin + +A basic installation of the events plugin looks as follows. + +```diff + // packages/backend/src/index.ts ++import { eventsPlugin } from '@backstage/plugin-events-backend'; + + const backend = createBackend(); ++backend.add(eventsPlugin()); +``` + +If you have other customizations made to `plugins/events.ts`, such as adding +custom subscribers, read on. Otherwise, you should be able to just delete that +file at this point. + +You will use the [extension points](../architecture/05-extension-points.md) +mechanism to extend or tweak the functionality of the plugin. To do that, +you'll make your own bespoke [module](../architecture/06-modules.md) which +depends on the appropriate extension point and interacts with it. + +```diff + // packages/backend/src/index.ts ++import { eventsExtensionPoint } from '@backstage/plugin-events-node'; ++import { createBackendModule } from '@backstage/backend-plugin-api'; + ++const eventsModuleCustomExtensions = createBackendModule({ ++ pluginId: 'events', // name of the plugin that the module is targeting ++ moduleId: 'customExtensions', ++ register(env) { ++ env.registerInit({ ++ deps: { ++ events: eventsExtensionPoint, ++ // ... and other dependencies as needed ++ }, ++ init({ events /* ..., other dependencies */ }) { ++ // Here you have the opportunity to interact with the extension ++ // point before the plugin itself gets instantiated ++ events.addSubscribers(new MySubscriber()); // just an example ++ }, ++ }); ++ }, ++}); + + const backend = createBackend(); + backend.add(eventsPlugin()); ++backend.add(eventsModuleCustomExtensions()); +``` + +This also requires that you have a dependency on the corresponding node package, +if you didn't already have one. + +```bash +# from the repository root +yarn add --cwd packages/backend @backstage/plugin-events-node +``` + +Here we've placed the module directly in the backend index file just to get +going easily, but feel free to move it out to where it fits best. As you migrate +your entire plugin flora to the new backend system, you will probably make more +and more of these modules as "first class" things, living right next to the +implementations that they represent, and being exported from there. + +### The Scaffolder Plugin + +A basic installation of the scaffolder plugin looks as follows. + +```diff + // packages/backend/src/index.ts ++import { scaffolderPlugin } from '@backstage/plugin-scaffolder-backend'; + + const backend = createBackend(); ++backend.add(scaffolderPlugin()); +``` + +If you have other customizations made to `plugins/scaffolder.ts`, such as adding +custom actions, read on. Otherwise, you should be able to just delete that file +at this point. + +You will use the [extension points](../architecture/05-extension-points.md) +mechanism to extend or tweak the functionality of the plugin. To do that, +you'll make your own bespoke [module](../architecture/06-modules.md) which +depends on the appropriate extension point and interacts with it. + +```diff + // packages/backend/src/index.ts ++import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node'; ++import { createBackendModule } from '@backstage/backend-plugin-api'; + ++const scaffolderModuleCustomExtensions = createBackendModule({ ++ pluginId: 'scaffolder', // name of the plugin that the module is targeting ++ moduleId: 'customExtensions', ++ register(env) { ++ env.registerInit({ ++ deps: { ++ scaffolder: scaffolderActionsExtensionPoint, ++ // ... and other dependencies as needed ++ }, ++ init({ scaffolder /* ..., other dependencies */ }) { ++ // Here you have the opportunity to interact with the extension ++ // point before the plugin itself gets instantiated ++ scaffolder.addActions(new MyAction()); // just an example ++ }, ++ }); ++ }, ++}); + + const backend = createBackend(); + backend.add(scaffolderPlugin()); ++backend.add(scaffolderModuleCustomExtensions()); +``` + +This also requires that you have a dependency on the corresponding node package, +if you didn't already have one. + +```bash +# from the repository root +yarn add --cwd packages/backend @backstage/plugin-scaffolder-node +``` + +Here we've placed the module directly in the backend index file just to get +going easily, but feel free to move it out to where it fits best. As you migrate +your entire plugin flora to the new backend system, you will probably make more +and more of these modules as "first class" things, living right next to the +implementations that they represent, and being exported from there. diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index fff82286ad..1556b6b377 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -6,7 +6,246 @@ sidebar_label: Overview description: Building backend plugins and modules using the new backend system --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + > NOTE: If you have an existing backend and/or backend plugins that are not yet > using the new backend system, see [migrating](./08-migrating.md). -# Overview +This section covers how to build your own backend [plugins](../architecture/04-plugins.md) and +[modules](../architecture/06-modules.md). They are sometimes collectively referred to as +backend _features_, and are the building blocks that adopters add to their +[backends](../architecture/02-backends.md). + +## Creating a new Plugin + +This guide assumes that you already have a Backend project set up. Even if you only want to develop a single plugin for publishing, we still recommend that you do so in a standard Backstage monorepo project, as you often end up needing multiple packages. For instructions on how to set up a new project, see our [getting started](../../getting-started/index.md#prerequisites) documentation. + +To create a Backend plugin, run `yarn new`, select `backend-plugin`, and fill out the rest of the prompts. This will create a new package at `plugins/<pluginId>-backend`, which will be the main entrypoint for your plugin. + +## Plugins + +A basic backend plugin might look as follows: + +```ts +import { + createBackendPlugin, + coreServices, +} from '@backstage/backend-plugin-api'; +import { createExampleRouter } from './router'; + +export const examplePlugin = createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + // Declare dependencies to services that you want to consume + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ + // Requested service instances get injected as per above + logger, + httpRouter, + }) { + // Perform your initialization and access the services as needed + const example = createExampleRouter(logger); + logger.info('Hello from example plugin'); + httpRouter.use(example); + }, + }); + }, +}); +``` + +When you depend on `plugin` scoped services, you'll receive an instance of them +that's specific to your plugin. In the example above, the logger might tag +messages with your plugin ID, and the HTTP router might prefix API routes with +your plugin ID, depending on the implementation used. + +See [the article on naming patterns](../architecture/07-naming-patterns.md) for +details on how to best choose names/IDs for plugins and related backend system +items. + +## Modules + +Backend modules are used to extend [plugins](../architecture/04-plugins.md) with +additional features or change existing behavior. They must always be installed +in the same backend instance as the plugin that they extend, and may only extend +a single plugin. Modules interact with their target plugin using the [extension +points](../architecture/05-extension-points.md) registered by the plugin, while also being +able to depend on the [services](../architecture/03-services.md) of that plugin. +That last point is worth reiterating: injected `plugin` scoped services will be +the exact +same ones as the target plugin will receive later, i.e. they will be scoped +using the target `pluginId` of the module. + +A module depends on the extension points exported by the target plugin's library +package, for example `@backstage/plugin-catalog-node`, and does not directly +declare a dependency on the plugin package itself. This is to avoid a direct +dependency and potentially cause duplicate installations of the plugin package, +while duplicate installations of library packages should always be supported. + +The following is an example of how to create a module that adds a new processor +using the `catalogProcessingExtensionPoint`: + +```ts +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { MyCustomProcessor } from './MyCustomProcessor'; + +export const catalogModuleExampleCustomProcessor = createBackendModule({ + moduleId: 'exampleCustomProcessor', + pluginId: 'catalog', + register(env) { + env.registerInit({ + deps: { + catalog: catalogProcessingExtensionPoint, + logger: coreServices.logger, + }, + async init({ catalog }) { + catalog.addProcessor(new MyCustomProcessor(logger)); + }, + }); + }, +}); +``` + +See [the article on naming patterns](../architecture/07-naming-patterns.md) for +details on how to best choose names/IDs for modules and related backend system +items. + +Notice that we're placing the extension point we want to interact with in the +`deps` option, while also depending on the logger service at the same time. When +initializing modules we can depend on both extension points and services +interchangeably. You can also depend on multiple extension points at once, in +case the implementation of the module requires it. + +It is typically best to keep modules slim and to each only add a single new +feature. It is often the case that it is better to create two separate modules +rather than one that provides both features. The one limitation here is that +modules can not interact with each other and need to be self contained. + +### HTTP Handlers + +Since modules have access to the same services as the plugin they extend, they +are also able to register their own HTTP handlers. For more information about +the HTTP service, see [core services](../core-services/01-index.md). When +registering HTTP handlers, it is important to try to avoid any future conflict +with the plugin itself, or other modules. A recommended naming pattern is to +register the handlers under the `/modules/<module-id>` path, where `<module-id>` +is the kebab-case ID of the module, for example +`/modules/example-custom-processor/v1/validators`. In a standard backend setup +the full path would then be +`<backendUrl>/api/catalog/modules/example-custom-processor/v1/validators`. + +### Database Access + +The same applies for modules that perform their own migrations and interact with +the database. They will run on the same logical database instance as the target +plugin, so care must be taken to choose table names that do not risk colliding +with those of the plugin. A recommended naming pattern is `<package +name>__<table name>`, for example the `@backstage/backend-tasks` package creates +tables named `backstage_backend_tasks__<table>`. If you use the default [`Knex` +migration facilities](https://knexjs.org/guide/migrations.html), you will also +want to make sure that it uses similarly prefixed migration state tables for its +internal bookkeeping needs, so they do not collide with the main ones used by +the plugin itself. You can do this as follows: + +```ts +await knex.migrate.latest({ + directory: migrationsDir, + tableName: 'backstage_backend_tasks__knex_migrations', +}); +``` + +## Customization + +There are several ways of configuring and customizing plugins and modules. + +### Extension Points + +Whenever you want to allow modules to configure your plugin dynamically, for +example in the way that the catalog backend lets catalog modules inject +additional entity providers, you can use the extension points mechanism. This is +described in detail with code examples in [the extension points architecture +article](../architecture/05-extension-points.md). + +### Configuration + +Your plugin or module can leverage the app configuration to configure its own +internal behavior. You do this by adding a dependency on `coreServices.config` +and reading from that. This pattern is a good fit especially for customization +that needs to be different across environments. + +```ts +import { coreServices } from '@backstage/backend-plugin-api'; + +export const examplePlugin = createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { config: coreServices.config }, + async init({ config }) { + // Here you can read from the current config as you see fit, e.g.: + const value = config.getOptionalString('example.value'); + }, + }); + }, +}); +``` + +Before adding custom configuration options, make sure to read [the configuration +docs](../../conf/index.md), in particular the section on [defining configuration +for your own plugins](../../conf/defining.md) which explains how to establish a +configuration schema for your specific plugin. + +### Options + +You'll have noted that the return values from `createBackendPlugin` and +`createBackendModule` are actually factory functions. These can be made to +accept options that shall be passed in at initialization time. + +This pattern can be a good fit for fairly simple, static configuration values. + +```ts +export interface ExampleOptions { + silent?: boolean; +} + +export const examplePlugin = createBackendPlugin( + (options?: ExampleOptions) => ({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + // Omitted dependencies but they remain the same as above + }, + async init( + { + /* ... */ + }, + ) { + // Here you can access the given options and act accordingly, e.g.: + if (!options?.silent) { + // ... + } + }, + }); + }, + }), +); +``` + +The return type from `createBackendPlugin` and `createBackendModule` will mimic +this, resulting in a factory function that accepts an optional options object. +You can also make it required to pass in options, by removing the optionality +(the question mark on the options) above. + +```ts +backend.add(examplePlugin({ silent: true })); +``` + +Use this pattern sparingly. There is a big convenience benefit in allowing +people to easily install backend plugins without having to always pass in a +large number of options, and these options cannot easily be made dynamic based +on the environment etc. diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index e8a8619f72..7f93fab45b 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -6,21 +6,163 @@ sidebar_label: Testing description: Learn how to test your backend plugins and modules --- -Utilities for testing backend plugins and modules are available in `@backstage/backend-test-utils`. -`startTestBackend` returns a server which can be used together with `supertest` to test the plugins. +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + +Utilities for testing backend plugins and modules are available in +`@backstage/backend-test-utils`. This section describes those facilities. + +## Testing Backend Plugins and Modules + +To facilitate testing of backend plugins and modules, the +`@backstage/backend-test-utils` package provides a `startTestBackend` function +which starts up an entire backend harness, complete with a number of mock +services. You can then provide overrides for services whose behavior you need to +adjust for the test run. The function also accepts a number of _features_ (a +collective term for backend [plugins](../architecture/04-plugins.md) and +[modules](../architecture/06-modules.md)), that are the subjects of the test. + +The function returns an HTTP server instance which can be used together with +e.g. `supertest` to easily test the actual REST service surfaces of plugins who +register routes with [the HTTP router service +API](../core-services/01-index.md). ```ts -import { startTestBackend } from '@backstage/backend-test-utils'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import request from 'supertest'; +import { myPlugin } from './plugin.ts'; + +describe('myPlugin', () => { + it('can serve values from config', async () => { + const fakeConfig = { myPlugin: { value: 7 } }; -describe('My plugin tests', () => { - it('should return 200', async () => { const { server } = await startTestBackend({ features: [myPlugin()], + services: [mockServices.config.factory({ data: fakeConfig })], }); - const response = await request(server).get('/api/example/hello'); + const response = await request(server).get('/api/example/get-value'); expect(response.status).toBe(200); + expect(response.body).toEqual({ value: 7 }); }); }); ``` + +This example shows how to access the mock service factories and +pass options to them, which will override the default mock services. + +The returned server also has a `port()` method which returns the dynamically +bound listening port. You can use this to perform lower level network +interactions with the running test service. + +## Testing Remote Service Interactions + +If your backend plugin or service interacts with external services using HTTP +calls, we recommend leveraging the `msw` package to intercept actual outgoing +requests and return mock responses. This lets you stub out remote services +rather than the local clients, leading to more thorough and robust tests. You +can read more about how it works [in their documentation](https://mswjs.io/). + +The `@backstage/backend-test-utils` package exports a `setupRequestMockHandlers` +function which ensures that the correct `jest` lifecycle hooks are invoked to +set up and tear down your `msw` instance, and enables the option that completely +rejects requests that don't match one of your mock rules. This ensures that your +tests cannot accidentally leak traffic into production from tests. + +Example: + +```ts +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +describe('read from remote', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + it('should auth and read successfully', async () => { + expect.assertions(1); + + worker.use( + rest.get('https://remote-server.com/api/v3/foo', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe('Bearer fake'); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.body(JSON.stringify({ value: 7 })), + ); + }), + ); + + // exercise your plugin or service as usual, with real clients + }); +}); +``` + +## Testing Database Interactions + +The `@backstage/backend-test-utils` package includes facilities for testing your +plugins' interactions with databases, including spinning up `testcontainers` +powered Docker images with real database engines to connect to. + +The base setup for such a test could look as follows: + +```ts +// MyDatabaseClass.test.ts +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { MyDatabaseClass, type FooTableRow } from './MyDatabaseClass'; + +describe('MyDatabaseClass', () => { + // Change this to the set of constants that you actually actively intend to + // support. This create call must be made inside a describe block. Make sure + // to create only one TestDatabases instance per file, since spinning up + // "physical" databases to test against is much costlier than creating the + // "logical" databases within them that the individual tests use. + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + // Just an example of how to conveniently bundle up the setup code + async function createSubject(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + const subject = new MyDatabaseClass({ database: knex }); + await subject.runMigrations(); + return { knex, subject }; + } + + describe('foo', () => { + // Easily run the exact same test onto all supported databases + it.each(databases.eachSupportedId())( + 'should run foo on %p', + async databaseId => { + const { knex, subject } = await createSubject(databaseId); + // raw knex is available for underlying manipulation + await knex<FooTableRow>('foo').insert({ value: 2 }); + // drive your system under test as usual + await expect(subject.foos()).resolves.toEqual([{ value: 2 }]); + }); + }); +``` + +If you want to pass the test database instance into backend plugins or services, +you can supply it in the form of a mock instance of `coreServices.database` to +your test database. + +```ts +const { knex, subject } = await createSubject(databaseId); +const { server } = await startTestBackend({ + features: [myPlugin()], + services: [[coreServices.database, { getClient: async () => knex }]], +}); +``` + +When running locally, the tests only run against SQLite for the sake of speed. +When the `CI` environment variable is set, all given database engines are used. + +If you do not want or are unable to use docker based database engines, e.g. if +your CI environment is able to supply databases natively, the `TestDatabases` +support custom connection strings through the use of environment variables that +it'll take into account when present. + +- `BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING` diff --git a/docs/backend-system/building-plugins-and-modules/08-migrating.md b/docs/backend-system/building-plugins-and-modules/08-migrating.md index 2f3fd5ebcc..cf5aa8106f 100644 --- a/docs/backend-system/building-plugins-and-modules/08-migrating.md +++ b/docs/backend-system/building-plugins-and-modules/08-migrating.md @@ -6,4 +6,231 @@ sidebar_label: Migration Guide description: How to migrate existing backend plugins to the new backend system --- -# Overview +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + +Migrating an existing backend plugin to the new backend system is fairly straightforward. The process is similar across the majority of plugins which just return a `Router` that is then wired up in the `index.ts` file of your backend. The primary thing that we need to do is to make sure that the dependencies that are required by the plugin are available, and then registering the router with the HTTP router service. + +Let's look at an example of migrating the Kubernetes backend plugin. In the existing (old) system, the kubernetes backend is structured like this: + +```ts +// @backstage/plugin-kubernetes-backend/src/service/router.ts + +import { KubernetesBuilder } from './KubernetesBuilder'; +export interface RouterOptions { + logger: Logger; + config: Config; + catalogApi: CatalogApi; + clusterSupplier?: KubernetesClustersSupplier; + discovery: PluginEndpointDiscovery; +} + +export async function createRouter( + options: RouterOptions, +): Promise<express.Router> { + const { router } = await KubernetesBuilder.createBuilder(options) + .setClusterSupplier(options.clusterSupplier) + .build(); + return router; +} +``` + +We can re-use the `router` created by the `KubernetesBuilder` in the new backend system. We only need to make sure that the dependencies specified in `RouterOptions` above are available. All of them are part of the `coreServices` which makes migration easy. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node'; +import { Router } from 'express'; +import { KubernetesBuilder } from './KubernetesBuilder'; + +export const kubernetesPlugin = createBackendPlugin({ + pluginId: 'kubernetes', + register(env) { + env.registerInit({ + deps: { + logger: coreServices.logger, + config: coreServices.config, + catalogApi: catalogServiceRef, + discovery: coreServices.discovery, + // The http router service is used to register the router created by the KubernetesBuilder. + http: coreServices.httpRouter, + }, + async init({ config, logger, catalogApi, discovery, http }) { + const { router } = await KubernetesBuilder.createBuilder({ + config, + logger, + catalogApi, + discovery, + }).build(); + + // We register the router with the http service. + http.use(router); + }, + }); + }, +}); +``` + +Done! Users of this plugin are now able to import the `kubernetesPlugin` and register it in their backend using + +```ts +// packages/backend/src/index.ts +import { kubernetesPlugin } from '@backstage/plugin-kubernetes-backend'; +backend.add(kubernetesPlugin); +``` + +There's one thing missing that those sharp eyed readers might have noticed: the `clusterSupplier` option is missing from the original plugin. Let's add it and discuss the alternatives. + +One alternative is to pass the `ClusterSupplier` in as options to the plugin, which is quick and easy but not very flexible, and also hard to evolve without introducing breaking changes as it changes the public API for the plugin. Having complex types passed in directly to the plugin also clutters the backend setup code and makes it harder to read. + +Options are primarily used for simple configuration values that are not complex types. In this case we want to allow users to register their own `ClusterSupplier` implementations to the plugin. This is where the new backend system's [extension points](../architecture/05-extension-points.md) come in handy, but let's look at doing this with options first. + +```ts +/* omitted imports but they remain the same as above */ + +export interface KubernetesOptions { + clusterSupplier?: KubernetesClustersSupplier; +} + +const kubernetesPlugin = createBackendPlugin((options: KubernetesOptions) => ({ + pluginId: 'kubernetes', + register(env) { + env.registerInit({ + deps: { + /* omitted dependencies but they remain the same as above */ + }, + async init({ config, logger, catalogApi, discovery, http }) { + const { router } = await KubernetesBuilder.createBuilder({ + config, + logger, + catalogApi, + discovery, + }) + .setClusterSupplier(options.clusterSupplier) + .build(); + http.use(router); + }, + }); + }, +})); +``` + +The above would allow users to specify their own `ClusterSupplier` implementation to the plugin like this: + +```ts +backend.add( + kubernetesPlugin({ clusterSupplier: new MyCustomClusterSupplier() }), +); +``` + +Just to echo what was said above, this is not a very flexible solution and will for example be problematic to keep backwards compatible if we start evolving the options to for example accept multiple suppliers or tweak the `ClusterSupplier` interface. + +The new [extension points](../architecture/05-extension-points.md) API allows [modules](../architecture/06-modules.md) to add functionality into the backend plugin itself, in this case an additional `ClusterSupplier`. + +The kubernetes backend plugin only supports one `ClusterSupplier` at this time but let's look at how we could add support for multiple suppliers using extension points. This allows users to install several modules that add their own `ClusterSupplier` implementations to the plugin like this: + +```ts +backend.add(kubernetesPlugin()); +backend.add(kubernetesGoogleContainerEngineClusterSupplier()); +backend.add(kubernetesElasticContainerEngine()); +``` + +Now let's look at how to implement this with extension points. First we need to define the extension point itself. As the extension point will be used by other modules, it's common practice to export these from a shared package so that they can be imported by other modules and plugins. + +We'll go ahead and create a `@backstage/plugin-kubernetes-node` package for this and from there we'll export the extension point. + +```ts +import { createExtensionPoint } from '@backstage/backend-plugin-api'; + +export interface KubernetesClusterSupplierExtensionPoint { + addClusterSupplier(supplier: KubernetesClustersSupplier): void; +} + +/** + * An extension point that allows other plugins to add cluster suppliers. + * @public + */ +export const kubernetesClustersSupplierExtensionPoint = + createExtensionPoint<KubernetesClusterSupplierExtensionPoint>({ + id: 'kubernetes.cluster-supplier', + }); +``` + +Now we can use this extension point in the kubernetes backend plugin to register the extension point for modules to use. + +```ts +import { kubernetesClustersSupplierExtensionPoint, KubernetesClusterSupplierExtensionPoint } from '@backstage/plugin-kubernetes-node'; + +// Our internal implementation of the extension point, should not be exported. +class ClusterSupplier implements KubernetesClusterSupplierExtensionPoint { + private clusterSuppliers: KubernetesClustersSupplier | undefined; + + // This method is private and only used internally to retrieve the registered supplier. + getClusterSupplier() { + return this.clusterSuppliers; + } + + addClusterSupplier(supplier: KubernetesClustersSupplier) { + // We can remove this check once the plugin support multiple suppliers. + if(this.clusterSuppliers) { + throw new Error('Multiple Kubernetes cluster suppliers is not supported at this time'); + } + this.clusterSuppliers = supplier; + } +} + +export const kubernetesPlugin = createBackendPlugin({ + pluginId: 'kubernetes', + register(env) { + const extensionPoint = new ClusterSupplier(); + // We register the extension point with the backend, which allows modules to + // register their own ClusterSupplier. + env.registerExtensionPoint( + kubernetesClustersSupplierExtensionPoint, + extensionPoint, + ); + + env.registerInit({ + deps: { + ... omitted ... + }, + async init({ config, logger, catalogApi, discovery, http }) { + const { router } = await KubernetesBuilder.createBuilder({ + config, + logger, + catalogApi, + discovery, + }) + // We pass in the registered supplier from the extension point. + .setClusterSupplier(extensionPoint.getClusterSupplier()) + .build(); + http.use(router); + }, + }); + }, +}); +``` + +And that's it! Modules can now be built that add clusters into to the kubernetes backend plugin, here's an example of a module that adds a `GoogleContainerEngineSupplier` to the kubernetes backend. + +```ts +import { kubernetesClustersSupplierExtensionPoint } from '@backstage/plugin-kubernetes-node'; + +export const kubernetesGoogleContainerEngineClusterSupplier = + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'gke.supplier', + register(env) { + env.registerInit({ + deps: { + supplier: kubernetesClustersSupplierExtensionPoint, + }, + async init({ supplier }) { + supplier.addClusterSupplier(new GoogleContainerEngineSupplier()); + }, + }); + }, + }); +``` diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index cb1ec157ae..c688055c96 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -1,11 +1,13 @@ --- id: index title: Core Backend Service APIs -sidebar_label: Core Services +sidebar_label: Overview # prettier-ignore description: Core backend service APIs --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + The default backend provides several [core services](https://github.com/backstage/backstage/blob/master/packages/backend-plugin-api/src/services/definitions/coreServices.ts) out of the box which includes access to configuration, logging, URL Readers, databases and more. All core services are available through the `coreServices` namespace in the `@backstage/backend-plugin-api` package. @@ -18,6 +20,8 @@ import { coreServices } from '@backstage/backend-plugin-api'; One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume. +### Using the service + The following example shows how to register a HTTP router for the `example` plugin. This single route will be available at the `/api/example/hello` path. @@ -29,7 +33,7 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { http: coreServices.httpRouter }, @@ -46,9 +50,33 @@ createBackendPlugin({ }); ``` -## Logging and Configuration Service +### Configuring the service -It is common for plugins to need access to configuration values and log messages. +There's additional configuration that you can optionally pass to setup the `httpRouter` core service. + +- `getPath` - Can be used to generate a path for each plugin. Currently defaults to `/api/${pluginId}` + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { httpRouterServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + httpRouterServiceFactory({ + getPath: (pluginId: string) => `/plugins/${pluginId}`, + }), + ], +}); +``` + +## Root HTTP Router + +The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. + +### Using the service + +The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. ```ts import { @@ -58,17 +86,588 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + rootHttpRouter: coreServices.rootHttpRouter, + }, + async init({ rootHttpRouter }) { + const router = Router(); + router.get('/health', (request, response) => { + response.send('OK'); + }); + + rootHttpRouter.use(router); + }, + }); + }, +}); +``` + +### Configuring the service + +There's additional options that you can pass to configure the root HTTP Router service. These options are passed when you call `createBackend`. + +- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. + +- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. + +You can configure the root HTTP Router service by passing the options to the `createBackend` function. + +```ts +import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + rootHttpRouterServiceFactory({ + configure: ({ app, middleware, routes, config, logger, lifecycle }) => { + // the built in middleware is provided through an option in the configure function + app.use(middleware.helmet()); + app.use(middleware.cors()); + app.use(middleware.compression()); + + // you can add you your own middleware in here + app.use(custom.logging()); + + // here the routes that are registered by other plugins + app.use(routes); + + // some other middleware that comes after the other routes + app.use(middleware.notFound()); + app.use(middleware.error()); + }, + }), + ], +}); +``` + +## Config + +This service allows you to read configuration values out of your `app-config` YAML files. + +### Using the service + +The following example shows how you can use the default config service to be able to get a config value, and then log it to the console. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', register(env) { env.registerInit({ deps: { log: coreServices.logger, config: coreServices.config, }, - async init({ config, log }) { - log.warn('Brace yourself for more log output'); - const url = config.getString('backend.baseUrl'); - log.info(`Backend URL is running on ${url}`); + async init({ log, config }) { + const baseUrl = config.getString('backend.baseUrl'); + log.warn(`The backend is running at ${baseUrl}`); + }, + }); + }, +}); +``` + +### Configuring the service + +There's additional configuration that you can optionally pass to setup the `config` core service. + +- `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` +- `remote` - Configure remote configuration loading + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { configServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + configServiceFactory({ + argv: [ + '--config', + '/backstage/app-config.development.yaml', + '--config', + '/backstage/app-config.yaml', + ], + remote: { reloadIntervalSeconds: 60 }, + }), + ], +}); +``` + +## Logging + +This service allows plugins to output logging information. There are actually two logger services: a root logger, and a plugin logger which is bound to individual plugins, so that you will get nice messages with the plugin ID referenced in the log lines. + +### Using the service + +The following example shows how to get the logger in your `example` backend plugin and create a warning message that will be printed nicely to the console. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + log: coreServices.logger, + }, + async init({ log }) { + log.warn("Here's a nice log line that's a warning!"); + }, + }); + }, +}); +``` + +### Root Logger + +The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. + +If you want to override the implementation for logging across all of the backend, this is the service that you should override. + +### Configuring the service + +The following example is how you can override the root logger service to add additional metadata to all log lines. + +```ts +import { coreServices } from '@backstage/backend-plugin-api'; +import { WinstonLogger } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + createServiceFactory({ + service: coreServices.rootLogger, + deps: { + config: coreServices.config, + }, + async factory({ config }) { + const logger = WinstonLogger.create({ + meta: { + service: 'backstage', + // here's some additional information that is not part of the + // original implementation + podName: 'myk8spod', + }, + level: process.env.LOG_LEVEL || 'info', + format: + process.env.NODE_ENV === 'production' + ? format.json() + : WinstonLogger.colorFormat(), + transports: [new transports.Console()], + }); + + return logger; + }, + }), + ], +}); +``` + +## Cache + +This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. + +### Using the service + +The following example shows how to get a cache client in your `example` backend plugin and setting and getting values from the cache. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + cache: coreServices.cache, + }, + async init({ cache }) { + const { key, value } = { key: 'test:key', value: 'bob' }; + await cache.set(key, value, { ttl: 1000 }); + + // .. some other stuff. + + await cache.get(key); // 'bob' + }, + }); + }, +}); +``` + +## Database + +This service lets your plugins get a `knex` client hooked up to a database which is configured in your `app-config` YAML files, for your persistence needs. + +If there's no config provided in `backend.database` then you will automatically get a simple in-memory SQLite 3 database for your plugin whose contents will be lost when the service restarts. + +This service is scoped per plugin too, so that table names do not conflict across plugins. + +### Using the service + +The following example shows how to get access to the database service in your `example` backend plugin and getting a client for interacting with the database. It also runs some migrations from a certain directory for your plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { resolvePackagePath } from '@backstage/backend-common'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + database: coreServices.database, + }, + async init({ database }) { + const client = await database.getClient(); + const migrationsDir = resolvePackagePath( + '@internal/my-plugin', + 'migrations', + ); + if (!database.migrations?.skip) { + await client.migrate.latest({ + directory: migrationsDir, + }); + } + }, + }); + }, +}); +``` + +## Discovery + +When building plugins, you might find that you will need to look up another plugin's base URL to be able to communicate with it. This could be for example an HTTP route or some `ws` protocol URL. For this we have a discovery service which can provide both internal and external base URLs for a given a plugin ID. + +### Using the service + +The following example shows how to get the discovery service in your `example` backend plugin and making a request to both the internal and external base URLs for the `derp` plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + discovery: coreServices.discovery, + }, + async init({ discovery }) { + const url = await discoverty.getBaseUrl('derp'); // can also use discovery.getBaseUrl to retrieve external URL + const response = await fetch(`${url}/hello`); + }, + }); + }, +}); +``` + +## Identity + +When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and to deconstruct them to get the user's entity ref and/or ownership claims out of them. + +### Using the service + +The following example shows how to get the identity service in your `example` backend plugin and retrieve the user's entity ref and ownership claims for the incoming request. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + http: coreServices.httpRouter, + }, + async init({ http, identity }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identity service to pull out the header from the request and get the user + const { + identity: { userEntityRef, ownershipEntityRefs }, + } = await identity.getIdentity({ + request, + }); + + // send the decoded and validated things back to the user + response.json({ + userEntityRef, + ownershipEntityRefs, + }); + }); + + http.use(router); + }, + }); + }, +}); +``` + +### Configuring the service + +There's additional configuration that you can optionally pass to setup the `identity` core service. + +- `issuer` - Set an optional issuer for validation of the JWT token +- `algorithms` - `alg` header for validation of the JWT token, defaults to `ES256`. More info on supported algorithms can be found in the [`jose` library documentation](https://github.com/panva/jose) + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { identityServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + identityServiceFactory({ + issuer: 'backstage', + algorithms: ['ES256', 'RS256'], + }), + ], +}); +``` + +## Lifecycle + +This service allows your plugins to register hooks for cleaning up resources as the service is shutting down (e.g. when a pod is being torn down, or when pressing `Ctrl+C` during local development). Other core services also leverage this same mechanism internally to stop themselves cleanly. + +### Using the service + +The following example shows how to get the lifecycle service in your `example` backend plugin to clean up a long running interval when the service is shutting down. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + lifecycle: coreServices.lifecycle, + logger: coreServices.logger, + }, + async init({ lifecycle, logger }) { + // some example work that we want to stop when shutting down + const interval = setInterval(async () => { + await fetch('http://google.com/keepalive').then(r => r.json()); + // do some other stuff. + }, 1000); + + lifecycle.addShutdownHook(() => clearInterval(interval)); + }, + }); + }, +}); +``` + +## Root Lifecycle + +This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks are collected and executed, so if you want to override the implementation of how those are processed, you should override this service. + +### Configure the service + +The following example shows how to override the default implementation of the lifecycle service with something that listens on different process events to the original. + +```ts +class MyCustomLifecycleService implements RootLifecycleService { + constructor(private readonly logger: LoggerService) {} + + #isCalled = false; + #shutdownTasks: Array<{ + hook: LifecycleServiceShutdownHook; + options?: LifecycleServiceShutdownOptions; + }> = []; + + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + this.#shutdownTasks.push({ hook, options }); + } + + async shutdown(): Promise<void> { + if (this.#isCalled) { + return; + } + this.#isCalled = true; + + this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); + await Promise.all( + this.#shutdownTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; + try { + await hook(); + logger.info(`Shutdown hook succeeded`); + } catch (error) { + logger.error(`Shutdown hook failed, ${error}`); + } + }), + ); + } +} + +const backend = createBackend({ + services: [ + createServiceFactory({ + service: coreServices.rootLifecycle, + deps: { + logger: coreServices.rootLogger, + }, + async factory({ logger }) { + return new MyCustomLifecycleService(logger); + }, + }), + ], +}); +``` + +## Permissions + +This service allows your plugins to ask [the permissions framework](https://backstage.io/docs/permissions/overview) for authorization of user actions. + +### Using the service + +The following example shows how to get the permissions service in your `example` backend to check to see if the user is allowed to perform a certain action with a custom permission rule. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + permissions: coreServices.permissions, + http: coreServices.httpRouter, + }, + async init({ permissions, http }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identity service to pull out the token from request headers + const { token } = await identity.getIdentity({ + request, + }); + + // ask the permissions framework what the decision is for the permission + const permissionResponse = await permissions.authorize( + [ + { + permission: myCustomPermission, + }, + ], + { token }, + ); + }); + + http.use(router); + }, + }); + }, +}); +``` + +## Scheduler + +When writing plugins, you sometimes want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin is running on. We supply a task scheduler for this purpose that is scoped per plugin so that you can create these tasks and orchestrate their execution. + +### Using the service + +The following example shows how to get the scheduler service in your `example` backend to issue a scheduled task that runs across your instances at a given interval. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + scheduler: coreServices.scheduler, + }, + async init({ scheduler }) { + await scheduler.scheduleTask({ + frequency: { minutes: 10 }, + timeout: { seconds: 30 }, + id: 'ping-google', + fn: async () => { + await fetch('http://google.com/ping'); + }, + }); + }, + }); + }, +}); +``` + +## URL Readers + +Plugins will require communication with certain integrations that users have configured. Popular integrations are things like Version Control Systems (VSC), such as GitHub, BitBucket GitLab etc. These integrations are configured in the `integrations` section of the `app-config.yaml` file. + +These URL readers are basically wrappers with authentication for files and folders that could be stored in these VCS repositories. + +### Using the service + +The following example shows how to get the URL Reader service in your `example` backend plugin to read a file and a directory from a GitHub repository. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import os from 'os'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + urlReader: coreServices.urlReader, + }, + async init({ urlReader }) { + const buffer = await urlReader + .read('https://github.com/backstage/backstage/blob/master/README.md') + .then(r => r.buffer()); + + const tmpDir = os.tmpdir(); + const directory = await urlReader + .readTree( + 'https://github.com/backstage/backstage/tree/master/packages/backend', + ) + .then(tree => tree.dir({ targetDir: tmpDir })); }, }); }, diff --git a/docs/backend-system/index.md b/docs/backend-system/index.md index ca13bc188c..87eb008129 100644 --- a/docs/backend-system/index.md +++ b/docs/backend-system/index.md @@ -1,15 +1,15 @@ --- id: index title: The Backend System -sidebar_label: Overview +sidebar_label: Introduction # prettier-ignore -description: The backend system +description: The Backend System --- -> **DISCLAIMER: The new backend system is under active development and is not considered stable** +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** ## Status -The new backend system is under active development, and only a small number of plugins have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet. +The new backend system is in alpha, and only a small number of plugins have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet. -You can find an example backend setup in [the backend-next package](https://github.com/backstage/backstage/tree/master/packages/backend-next). +You can find an example backend setup in [the `backend-next` package](https://github.com/backstage/backstage/tree/master/packages/backend-next). diff --git a/docs/dls/design.md b/docs/dls/design.md index a2091aeb10..3444ff3ad2 100644 --- a/docs/dls/design.md +++ b/docs/dls/design.md @@ -26,7 +26,7 @@ that growth, check out the issues in this GitHub repo! The Backstage Design Team is small but mighty, and we truly cherish the amazing opportunity we have to work with the Backstage Open Source community! Have an idea? A component request? Feel free to communicate with us via -[Discord](https://discord.gg/EBHEGzX) (_#design_ channel). Collaboration trumps +[Discord](https://discord.gg/backstage-687207715902193673) (_#design_ channel). Collaboration trumps individual speed, and we want to work with you to make Backstage work for all of our users. @@ -38,7 +38,7 @@ loop! Keep an eye on our where we're headed and review the [open design issues](https://github.com/backstage/backstage/issues?q=is%3Aopen+is%3Aissue+label%3Adesign), to see if you can help. We'll also be posting updates in the _#design_ channel -on [Discord](https://discord.gg/EBHEGzX). Not only that, we want to keep you +on [Discord](https://discord.gg/backstage-687207715902193673). Not only that, we want to keep you informed on the decisions we've made and why we've made them. ## 🛠 Our Practice @@ -101,14 +101,14 @@ focused on building up our core Backstage user experience. We’ll be labeling o issues in GitHub with ‘design’ and/or ‘storybook’ - so feel free to browse and tackle the tasks that interest you. If you have any questions regarding an issue, you can ask them in the comments section of the issue or on -[Discord](https://discord.gg/EBHEGzX). We absolutely adore our external +[Discord](https://discord.gg/backstage-687207715902193673). We absolutely adore our external contributors and will send you virtual [semlas](https://en.wikipedia.org/wiki/Semla) for your contributions! ### Request a component Create an issue (label it design and assign it to katz95) or send us a message -on [Discord](https://discord.gg/EBHEGzX) (_#design_ channel) with details of +on [Discord](https://discord.gg/backstage-687207715902193673) (_#design_ channel) with details of what the component is and its relevant use cases. Your request will be reviewed by our design team and you should hear back from us within 1-2 business days. We’ll get back to you and let you know whether your requested component will get @@ -124,7 +124,7 @@ components we’ve designed to the Storybook as well. Community to share our design assets. You can duplicate our UI Kit and design your own plugin for Backstage. -**[Discord](https://discord.gg/EBHEGzX)** - all design questions should be +**[Discord](https://discord.gg/backstage-687207715902193673)** - all design questions should be directed to the _#design_ channel. **[Customize Backstage's look and feel](https://backstage.io/docs/getting-started/app-custom-theme)** - @@ -142,4 +142,4 @@ scenes, we’re setting up a few foundational elements to make sure that contributing to Backstage as a designer is easy. From styling guidelines to UX principles to Figma documents, we’ll make sure you’re equipped to chip in on this project. We’re excited to work with you! In the meantime, we’d love to hear -from you on [Discord](https://discord.gg/EBHEGzX). +from you on [Discord](https://discord.gg/backstage-687207715902193673). diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 41d72b2be2..837574aa9e 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -402,6 +402,41 @@ view the Kubernetes API docs for your Kubernetes version (e.g. [API Groups for v1.22](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#-strong-api-groups-strong-) ) +### `objectTypes` (optional) + +Overrides for the Kubernetes object types fetched from the cluster. The default object types are: + +- pods +- services +- configmaps +- limitranges +- deployments +- replicasets +- horizontalpodautoscalers +- jobs +- cronjobs +- ingresses +- statefulsets +- daemonsets + +You may use this config to override the default object types if you only want a subset of +the default ones. However, it's currently not supported to fetch object types other +than the ones specified in the default types. + +Example: + +```yaml +--- +kubernetes: + objectTypes: + - configmaps + - deployments + - limitranges + - pods + - services + - statefulsets +``` + ### Role Based Access Control The current RBAC permissions required are read-only cluster wide, the below @@ -441,6 +476,13 @@ rules: - get - list - watch + - apiGroups: + - metrics.k8s.io + resources: + - pods + verbs: + - get + - list ``` ## Surfacing your Kubernetes components as part of an entity diff --git a/docs/features/kubernetes/proxy.md b/docs/features/kubernetes/proxy.md new file mode 100644 index 0000000000..5848d62e3e --- /dev/null +++ b/docs/features/kubernetes/proxy.md @@ -0,0 +1,87 @@ +--- +id: proxy +title: Kubernetes Backend Proxy Endpoint +sidebar_label: Proxy +description: Interacting with the Kubernetes API in Backstage plugins +--- + +[Contributors](https://backstage.io/docs/overview/glossary#backstage-user-profiles) wanting to +create developer portal experiences based on data from Kubernetes (e.g. for +interacting with [Custom +Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) +beyond the default behaviors of the existing Kubernetes plugin) can leverage the +Kubernetes backend plugin's proxy endpoint to allow them to make arbitrary +requests to the [REST +API](https://kubernetes.io/docs/reference/using-api/api-concepts/). + +Here is a snippet fetching namespaces from a cluster configured with the +`google` [auth provider](https://backstage.io/docs/features/kubernetes/configuration#clustersauthprovider): + +```typescript +import { + discoveryApiRef, + googleAuthApiRef, + useApi, +} from '@backstage/core-plugin-api'; + +const CLUSTER_NAME = ''; // use a known cluster name + +// get a bearer token from Google +const googleAuthApi = useApi(googleAuthApiRef); +const token = await googleAuthApi.getAccessToken( + 'https://www.googleapis.com/auth/cloud-platform', +); + +const discoveryApi = useApi(discoveryApiRef); +const kubernetesBaseUrl = await discoveryApi.getBaseUrl('kubernetes'); +const kubernetesProxyEndpoint = `${kubernetesBaseUrl}/proxy`; + +// fetch namespaces +await fetch(`${kubernetesProxyEndpoint}/api/v1/namespaces`, { + method: 'GET', + headers: { + 'X-Kubernetes-Cluster': CLUSTER_NAME, + Authorization: `Bearer ${token}`, + }, +}); +``` + +## How it works + +The proxy will interpret the +[`X-Kubernetes-Cluster` +header](https://backstage.io/docs/reference/plugin-kubernetes-backend.header_kubernetes_cluster) +as the name of the cluster to target. This name will be compared to each cluster +returned by all the configured [cluster +locators](https://backstage.io/docs/features/kubernetes/configuration#clusterlocatormethods) +-- the first cluster whose [`name` field](https://backstage.io/docs/features/kubernetes/configuration#clustersname) matches +the value in the header will be targeted. + +Then the request will be forwarded verbatim (but with the endpoint's base URL +prefix stripped) to the cluster. + +## Authentication + +Until some security and permission decisions are made (see [this +conversation](https://github.com/backstage/backstage/pull/13026/files#r1029376939) +for context), contributors consuming the proxy endpoint in their plugin code are +responsible for negotiating their own bearer token out-of-band. This requires +knowing some auth details about the cluster being contacted -- in practice, only +clusters with [client side auth +providers](https://backstage.io/docs/features/kubernetes/authentication#client-side-providers) can reasonably be reached. + +The proxy has no provisions for mTLS, so it cannot be used to connect to +clusters using the [x509 Client +Certs](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#x509-client-certs) +authentication strategy. [Bearer +tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#putting-a-bearer-token-in-a-request) +will be forwarded as-is. + +## Other known limitations + +The proxy as it was released in [Backstage +1.9](https://github.com/backstage/backstage/blob/master/docs/releases/v1.9.0-changelog.md#patch-changes-15) +has a known bug: + +- [#15901](https://github.com/backstage/backstage/issues/15901) - it cannot + reliably target clusters who share the same name with another located cluster. diff --git a/docs/features/search/architecture.md b/docs/features/search/architecture.md index f1183cec5f..62f3a68ff6 100644 --- a/docs/features/search/architecture.md +++ b/docs/features/search/architecture.md @@ -9,7 +9,9 @@ to support a wide variety of search engines, while providing a simple developer experience for plugin developers, and a good out-of-the-box experience for Backstage end-users. -<img data-zoomable src="../../assets/search/architecture.drawio.svg" alt="Search Architecture" /> +<!-- todo: Needs zoomable plugin --> + +![Search Architecture](../../assets/search/architecture.drawio.svg) At a base-level, we want to support the following: diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 7267e9fd5b..b464168724 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -114,6 +114,42 @@ of the `SearchType` component. > Check out the documentation around [integrating search into plugins](../../plugins/integrating-search-into-plugins.md#create-a-collator) for how to create your own collator. +## How to customize fields in the Software Catalog index + +Sometimes you will might want to have ability to control +which data passes to search index in catalog collator, or to customize data for specific kind. +You can easily do that by passing `entityTransformer` callback to `DefaultCatalogCollatorFactory`. +You can either just simply amend default behaviour, or even to write completely new document +(which should follow some required basic structure though). + +> `authorization` and `location` cannot be modified via a `entityTransformer`, `location` can be modified only through `locationTemplate`. + +```diff +// packages/backend/src/plugins/search.ts + +const entityTransformer: CatalogCollatorEntityTransformer = (entity: Entity) => { + if (entity.kind === 'SomeKind') { + return { + // customize here output for 'SomeKind' kind + }; + } + + return { + // and customize default output + ...defaultCatalogCollatorEntityTransformer(entity), + text: 'my super cool text', + }; +}; + +indexBuilder.addCollator({ + collator: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, ++ entityTransformer, + }), +}); +``` + ## How to limit what can be searched in the Software Catalog The Software Catalog includes a wealth of information about the components, @@ -174,3 +210,170 @@ const highlightOverride = { [obj-mode]: https://nodejs.org/dist/latest-v16.x/docs/api/stream.html#stream_object_mode [read-stream]: https://nodejs.org/dist/latest-v16.x/docs/api/stream.html#readable-streams [async-gen]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#iterating_over_async_generators + +## How to render search results using extensions + +Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages: + +### 1. Providing an extension in your plugin package + +Using the example below, you can provide an extension to be used as a default result item: + +```tsx +// plugins/your-plugin/src/plugin.ts +import { createPlugin } from '@backstage/core-plugin-api'; +import { createSearchResultListItemExtension } from '@backstage/plugin-search-react'; + +const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' }); + +export const YourSearchResultListItemExtension = plugin.provide( + createSearchResultListItemExtension({ + name: 'YourSearchResultListItem', + component: () => + import('./components').then(m => m.YourSearchResultListItem), + }), +); +``` + +If your list item accept props, you can extend the `SearchResultListItemExtensionProps` with your component specific props: + +```tsx +export const YourSearchResultListItemExtension: ( + props: SearchResultListItemExtensionProps<YourSearchResultListItemProps>, +) => JSX.Element | null = plugin.provide( + createSearchResultListItemExtension({ + name: 'YourSearchResultListItem', + component: () => + import('./components').then(m => m.YourSearchResultListItem), + }), +); +``` + +Additionally, you can define a predicate function that receives a result and returns whether your extension should be used to render it or not: + +```tsx +// plugins/your-plugin/src/plugin.ts +import { createPlugin } from '@backstage/core-plugin-api'; +import { createSearchResultListItemExtension } from '@backstage/plugin-search-react'; + +const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' }); + +export const YourSearchResultListItemExtension = plugin.provide( + createSearchResultListItemExtension({ + name: 'YourSearchResultListItem', + component: () => + import('./components').then(m => m.YourSearchResultListItem), + // Only results matching your type will be rendered by this extension + predicate: result => result.type === 'YOUR_RESULT_TYPE', + }), +); +``` + +Remember to export your new extension: + +```tsx +// plugins/your-plugin/src/index.ts +export { YourSearchResultListItem } from './plugin.ts'; +``` + +For more details, see the [createSearchResultListItemExtension](https://backstage.io/docs/reference/plugin-search-react.createsearchresultlistitemextension) API reference. + +### 2. Using an extension in your Backstage app + +Now that you know how a search result item is provided, let's finally see how they can be used, for example, to compose a page in your application: + +```tsx +// packages/app/src/components/searchPage.tsx +import React from 'react'; + +import { Grid, Paper } from '@material-ui/core'; +import BuildIcon from '@material-ui/icons/Build'; + +import { + Page, + Header, + Content, + DocsIcon, + CatalogIcon, +} from '@backstage/core-components'; +import { SearchBar, SearchResult } from '@backstage/plugin-search-react'; + +// Your search result item extension +import { YourSearchResultListItem } from '@backstage/your-plugin'; + +// Extensions provided by other plugin developers +import { ToolSearchResultListItem } from '@backstage/plugin-explore'; +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; +import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized'; + +// This example omits other components, like filter and pagination +const SearchPage = () => ( + <Page themeId="home"> + <Header title="Search" /> + <Content> + <Grid container direction="row"> + <Grid item xs={12}> + <Paper> + <SearchBar /> + </Paper> + </Grid> + <Grid item xs={12}> + <SearchResult> + <YourSearchResultListItem /> + <CatalogSearchResultListItem icon={<CatalogIcon />} /> + <TechDocsSearchResultListItem icon={<DocsIcon />} /> + <ToolSearchResultListItem icon={<BuildIcon />} /> + </SearchResult> + </Grid> + </Grid> + </Content> + </Page> +); + +export const searchPage = <SearchPage />; +``` + +> **Important**: A default result item extension should be placed as the last child, so it can be used only when no other extensions match the result being rendered. If a non-default extension is specified, the `DefaultResultListItem` component will be used. + +As another example, here's a search modal that renders results with extensions: + +```tsx +// packages/app/src/components/searchModal.tsx +import React from 'react'; + +import { DialogContent, DialogTitle, Paper } from '@material-ui/core'; +import BuildIcon from '@material-ui/icons/Build'; + +import { DocsIcon, CatalogIcon } from '@backstage/core-components'; +import { SearchBar, SearchResult } from '@backstage/plugin-search-react'; + +// Your search result item extension +import { YourSearchResultListItem } from '@backstage/your-plugin'; + +// Extensions provided by other plugin developers +import { ToolSearchResultListItem } from '@backstage/plugin-explore'; +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; +import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized'; + +export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => ( + <> + <DialogTitle> + <Paper> + <SearchBar /> + </Paper> + </DialogTitle> + <DialogContent> + <SearchResult onClick={toggleModal}> + <CatalogSearchResultListItem icon={<CatalogIcon />} /> + <TechDocsSearchResultListItem icon={<DocsIcon />} /> + <ToolSearchResultListItem icon={<BuildIcon />} /> + {/* As a "default" extension, it does not define a predicate function, + so it must be the last child to render results that do not match the above extensions */} + <YourSearchResultListItem /> + </SearchResult> + </DialogContent> + </> +); +``` + +There are other more specific search results layout components that also accept result item extensions, check their documentation: [SearchResultList](https://backstage.io/storybook/?path=/story/plugins-search-searchresultlist--with-result-item-extensions) and [SearchResultGroup](https://backstage.io/storybook/?path=/story/plugins-search-searchresultgroup--with-result-item-extensions). diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index da4eb3d229..5fc2537712 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -89,23 +89,23 @@ export class FrobsProvider implements EntityProvider { private readonly reader: UrlReader; private connection?: EntityProviderConnection; - /** [1] **/ + /** [1] */ constructor(env: string, reader: UrlReader) { this.env = env; this.reader = reader; } - /** [2] **/ + /** [2] */ getProviderName(): string { return `frobs-${this.env}`; } - /** [3] **/ + /** [3] */ async connect(connection: EntityProviderConnection): Promise<void> { this.connection = connection; } - /** [4] **/ + /** [4] */ async run(): Promise<void> { if (!this.connection) { throw new Error('Not initialized'); @@ -116,10 +116,10 @@ export class FrobsProvider implements EntityProvider { ); const data = JSON.parse(await response.buffer()).toString(); - /** [5] **/ + /** [5] */ const entities: Entity[] = frobsToEntities(data); - /** [6] **/ + /** [6] */ await this.connection.applyMutation({ type: 'full', entities: entities.map(entity => ({ diff --git a/docs/features/software-catalog/system-model.md b/docs/features/software-catalog/system-model.md index 409f3e8450..af359bb18a 100644 --- a/docs/features/software-catalog/system-model.md +++ b/docs/features/software-catalog/system-model.md @@ -63,6 +63,17 @@ BigTable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and systems will better allow us to visualize resource footprint, and create tooling around them. +## Organizational Entities + +### User + +A user describes a person, such as an employee, a contractor, or similar. + +### Group + +A group describes an organizational entity, such as for example a team, a +business unit, or a loose collection of people in an interest group. + ## Ecosystem Modeling A large catalogue of components, APIs and resources can be highly granular and @@ -108,3 +119,20 @@ domain would come with some documentation on how to accept payments for a new product or use-case, share the same entity types in their APIs, and integrate well with each other. Other domains could be “Content Ingestion”, “Ads” or “Search”. + +## Other + +### Location + +A location is a marker that references other places to look for catalog data. + +### Type + +The type field in the system has no set meaning. It is up to the user to assign their own types and use them as desired, such as for link validation or creating custom UI components. Some common pre-defined types are depicted in the +[ecosystem modeling diagram](#ecosystem-modeling). + +### Template + +A template definition describes both the parameters that are rendered in the +frontend part of the scaffolding wizard, and the steps that are executed when +scaffolding that component. diff --git a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md index 6e37a4df3e..f9d2c4eb0c 100644 --- a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md +++ b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md @@ -211,7 +211,7 @@ input: ### Summary -Of course, we're always available on [discord](https://discord.gg/MUpMjP2) if +Of course, we're always available on [discord](https://discord.gg/backstage-687207715902193673) if you're stuck or something's not working as expected. You can also [raise an issue](https://github.com/backstage/backstage/issues/new/choose) with feedback or bugs! diff --git a/docs/features/software-templates/testing-scaffolder-alpha.md b/docs/features/software-templates/testing-scaffolder-alpha.md new file mode 100644 index 0000000000..76479b137e --- /dev/null +++ b/docs/features/software-templates/testing-scaffolder-alpha.md @@ -0,0 +1,234 @@ +--- +id: testing-scaffolder-alpha +title: 'Experimental: Testing out the alpha Scaffolder plugin' +# prettier-ignore +description: Docs on the upcoming breaking release for the scaffolder plugin +--- + +## What's `scaffolder/next`? + +The `alpha` version, or as you might have seen referred to in other places the `scaffolder/next` release, is a new version of the `scaffolder` plugin that will be the first breaking change to the plugin, so you can also think of it as `@backstage/plugin-scaffolder@2.0.0`. +Its mostly a rewrite of a lot of the frontend components and pages that had very limited test coverage, which made adding new features to the `scaffolder` plugin quite hard, and we were lacking in confidence when making changes. + +There is of course some other things that have changed when re-writing this, which are essentially what has caused some breaking changes. +Now, this is not like previous scaffolder changes where you would have to change all of your templates as this is only the frontend plugin that is going to have breaking changes. You can read more about the [breaking changes](#breaking-changes) below. + +## What's new? + +First off, the main dependency that we have for the frontend which is responsible for rendering the `JSONSchema` into `material-ui` components is [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form). +This dependency in the current version of the plugin is 3.x.x, which is now 2 major versions out of date. Long story short, `v4` of this plugin contained some bug fixes, and new features but we we're unable to upgrade due to some issues with having support for `material-ui@v4`, so we had to wait for `v5` to be released, and because of the `FieldExtensions` and how they are very tightly coupled to the `react-jsonschema-form` library, we also wanted to make sure that this release was stable before getting people to migrate their `Field Extensions`. + +With that in mind, this release has `v5` of `react-jsonschema-form`, and with that comes all the new features and bugfixes in `v4` that we were waiting for - one of the main ones being the ability to use `if / then / else` syntax in the `template.yaml` definitions! 🎉 + +We've also rebuilt how validation works in the `scaffolder` components, which now means that we've opened the ability to have `async` validation functions in your `Field Extensions`. + +Some of the pages have gotten a little bit of an overhaul in terms of UI based on some research and feedback from the community and internally. + +- The `TemplateList` page has gotten some new `Card` components which show a little more information than the previous version with a little `material-ui` standards. +- The `WizardPage` has received some new updates with the stepper now running horizontally, and the `Review` step being a dedicated step in the stepper. +- The `OngoingTask` page now does not show the logs by default, and instead has a much cleaner interface for tracking the ongoing steps and the pipeline of actions that are currently showing. + - You can also now provide your own `OutputsComponent` which can be used to render the outputs from an ongoing / completed task in a way that suits your templates the best. For instance, if your template produces `Pull Requests`, it could be useful to render these in an interactive way where you can see the statuses of each of these `Pull Requests` in the `Ongoing Task` page. + +There's also a lot of bug fixes, and other things, but these are the main ones that we wanted to highlight. + +## How do I test out the `alpha` version? + +With the release of [`v1.11.0`](https://github.com/backstage/backstage/releases/tag/v1.11.0) it's now possible to run the `scaffolder/next` plugin and it be a drop in replacement for the current version that you use today. This means that you can start using the new code, and start testing it out. Once we have collected enough feedback, and squashed any bugs that might block us from releasing, it will be promoted from the `/alpha` exports and replace the existing code leading to breaking changes if you haven't already made these changes as part of this testing pilot. Those that have chosen to opt into this testing pilot means that once we promote it from the `/alpha` exports, you will need to update your code to point to the original exports from the `scaffolder` plugin, just like the code is today but with the [breaking changes](#breaking-changes) that you already made to your `Custom Field Extensions`. + +It's also worth calling out that if you do test this out, and find some issues or something not working out as expected, feel free to raise an issue in the [repo](https://github.com/backstage/backstage) or reach out to us on Discord! + +### Make the required changes to `App.tsx` + +The `ScaffolderPage` router has a completely different export for the `scaffolder/next` work, so you will want to change any import from the old `ScaffolderPage` to the new `NextScaffolderPage` + +```diff +- import { ScaffolderPage } from '@backstage/plugin-scaffolder'; ++ import { NextScaffolderPage } from '@backstage/plugin-scaffolder/alpha'; + +``` + +And this API should be the exact same as the previous Router, so you should be able to make a change like the following further down in this file: + +```diff + <Route + path="/create" + element={ +- <ScaffolderPage ++ <NextScaffolderPage + groups={[ + { + title: 'Recommended', + filter: entity => + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ]} + /> + } + > + <ScaffolderFieldExtensions> + <LowerCaseValuePickerFieldExtension /> + ... other extensions + </ScaffolderFieldExtensions> + <ScaffolderLayouts> + <TwoColumnLayout /> + ... other layouts + </ScaffolderLayouts> + </Route> +``` + +### Make the required changes to your `CustomFieldExtensions` + +There's differently named function for creating field extensions part of the `/alpha` exports as these are the ones that can contain breaking changes because of the breaking changes that have been applied in `react-jsonschema-form`. + +Let's take the following example: + +```ts +export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + component: EntityNamePicker, + name: 'EntityNamePicker', + validation: entityNamePickerValidation, + schema: EntityNamePickerSchema, + }), +); +``` + +References for `createScaffolderFieldExtension` have an `/alpha` version of `createNextScaffolderFieldExtension`, which should be used instead. + +```diff +-import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder'; ++import { createNextScaffolderFieldExtension } from '@backstage/plugin-scaffolder/alpha'; + +export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( +- createScaffolderFieldExtension({ ++ createNextScaffolderFieldExtension({ + component: EntityNamePicker, + name: 'EntityNamePicker', + validation: entityNamePickerValidation, + }), +); +``` + +Once you've done this you will find that you will have two squiggly lines under the properties that are passed in. One for the component and one for the validation (if provided.) + +Let's take the following code for the `EntityNamePicker` component: + +```ts +export const EntityNamePicker = ( + props: FieldExtensionComponentProps<string, EntityNamePickerProps>, +) => { + const { + onChange, + required, + schema: { title = 'Name', description = 'Unique name of the component' }, + rawErrors, + formData, + uiSchema: { 'ui:autofocus': autoFocus }, + idSchema, + placeholder, + } = props; + ... +} +``` + +There's another `/alpha` export that you need to replace `FieldExtensionComponentProps` with which is the `NextFieldExtensionComponentProps`. + +```diff +- import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; ++ import { NextFieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react/alpha'; + +export const EntityNamePicker = ( +- props: FieldExtensionComponentProps<string, EntityNamePickerProps>, ++ props: NextFieldExtensionComponentProps<string, EntityNamePickerProps>, +) => { + const { + onChange, + required, + schema: { title = 'Name', description = 'Unique name of the component' }, + rawErrors, + formData, +- uiSchema: { 'ui:autofocus': autoFocus }, ++ uiSchema: { 'ui:autofocus': autoFocus } = {}, + idSchema, + placeholder, + } = props; + ... +} +``` + +You'll notice that there's an additional change here, which is that we're now defaulting the `uiSchema` to an empty object. This is because the `uiSchema` is now optional, and if you don't provide it, it will be `undefined` instead of an empty object. There's more around this in the [breaking changes](#breaking-changes) section. + +To fix the previous validation error, you will need to change the import for the `FieldValidation` type that is used in the `validation` function. + +Let's take the following example of the validation function: + +```ts +import { FieldValidation } from '@rjsf/utils'; +import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; + +export const entityNamePickerValidation = ( + value: string, + validation: FieldValidation, +) => { + if (!KubernetesValidatorFunctions.isValidObjectName(value)) { + validation.addError( + 'Must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', + ); + } +}; +``` + +You will need to change the import for `FieldValidation` to point at the new `react-jsonschema-form` dependency. + +> Note: you will probably need to install this dependency too, by using `yarn add @rjsf/utils` in the package where you define these validation functions, this could also be in the `packages/app` folder, so you can install it there if needed. + +```diff +- import { FieldValidation } from '@rjsf/core'; ++ import { FieldValidation } from '@rjsf/utils; +import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; + +export const entityNamePickerValidation = ( + value: string, + validation: FieldValidation, +) => { +``` + +## Breaking Changes + +Once we fully release the code that is in the `/alpha` exports right now onto the current API and release v2.0.0 of `@backstage/plugin-scaffolder` the breaking changes will be as follows: + +### `uiSchema` is now optional + +Later releases of `react-jsonschema-form` have made the `uiSchema` optional, and if you don't provide it, it will be `undefined` instead of an empty object. This means that you will need to make sure that you're defaulting the `uiSchema` to an empty object if you're using it in your code. + +```diff + const { + onChange, + required, + schema: { title = 'Name', description = 'Unique name of the component' }, + rawErrors, + formData, +- uiSchema: { 'ui:autofocus': autoFocus }, ++ uiSchema: { 'ui:autofocus': autoFocus } = {}, + idSchema, + placeholder, + } = props; +``` + +### `formData` can also be `undefined` + +If you were using the `formData` and assuming that it was set to an empty object when building `Field Extensions` that return objects, then this will be `undefined` now due to a change in the `react-jsonschema-form` library. + +```diff + const { + onChange, + required, + schema: { title = 'Name', description = 'Unique name of the component' }, + rawErrors, +- formData, ++ formData = {}, // or maybe some other default value that you would prefer + uiSchema: { 'ui:autofocus': autoFocus } = {}, + idSchema, + placeholder, + } = props; +``` diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index dfbd4b0a42..ffe3312501 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -168,5 +168,6 @@ scaffolder backend: | Scaffolder Git Actions | [plugin-scaffolder-git-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-git-actions) | [Drew Hill](https://github.com/arhill05) | | Azure Pipeline Actions | [scaffolder-backend-module-azure-pipelines](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) | | Azure Repository Actions | [scaffolder-backend-module-azure-repositories](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-repositories) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) | +| Snyk Import Project | [plugin-scaffolder-backend-module-snyk](https://www.npmjs.com/package/@ma11hewthomas/plugin-scaffolder-backend-module-snyk) | [Matthew Thomas](https://github.com/Ma11hewThomas) | Have fun! 🚀 diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index be00453795..20575a0b4a 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -35,7 +35,7 @@ import FormControl from '@material-ui/core/FormControl'; /* This is the actual component that will get rendered in the form */ -export const ValidateKebabCaseExtension = ({ +export const ValidateKebabCase = ({ onChange, rawErrors, required, @@ -96,7 +96,7 @@ import { import { ValidateKebabCase, validateKebabCaseValidation, -} from './ValidateKebabCase'; +} from './ValidateKebabCase/ValidateKebabCaseExtension'; export const ValidateKebabCaseFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ @@ -173,7 +173,7 @@ spec: title: Name type: string description: My custom name for the component - ui:field: ValidateKebabCaseExtension + ui:field: ValidateKebabCase steps: [...] ``` diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 71aa0ab758..a272f05f3f 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -112,11 +112,11 @@ steps which would be rendered as different steps in the scaffolder plugin frontend. Each `Step` is `JSONSchema` with some extra goodies for styling what it might -look like in the frontend. For these steps we rely very heavily on this library: -https://github.com/rjsf-team/react-jsonschema-form. They have some great docs -too here: https://react-jsonschema-form.readthedocs.io/ and a playground where -you can play around with some examples here -https://rjsf-team.github.io/react-jsonschema-form. +look like in the frontend. For these steps we rely very heavily on this +[library](https://github.com/rjsf-team/react-jsonschema-form). They have some +[great docs](https://rjsf-team.github.io/react-jsonschema-form/docs/) and a +[playground](https://rjsf-team.github.io/react-jsonschema-form) where you can +play around with some examples. There's another option for that library called `uiSchema` which we've taken advantage of, and we've merged it with the existing `JSONSchema` that you diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index fe1058b2ca..bd709ee15f 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -39,7 +39,9 @@ representative of physical spaces in the TechDocs UI: - `Component`: A [proposed-but-not-yet-implemented](https://github.com/backstage/backstage/issues/11109) virtual location, aimed at simplifying a common type of Addon. -<img data-zoomable src="../../assets/techdocs/addon-locations.png" alt="TechDocs Addon Location Guide" /> +<!-- todo: Needs zoomable plugin --> + +![TechDocs Addon Location Guide](../../assets/techdocs/addon-locations.png) ### Addon Registry @@ -125,6 +127,7 @@ discover available Addons, we've compiled a list of them here: | [`<ExpandableNavigation />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | | [`<ReportIssue />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | | [`<TextSize />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | +| [`<LightBox />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.lightbox) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. | Got an Addon to contribute? Feel free to add a row above! diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md index d781f71917..2cb8543ccd 100644 --- a/docs/features/techdocs/architecture.md +++ b/docs/features/techdocs/architecture.md @@ -9,7 +9,9 @@ description: Documentation on TechDocs Architecture When you deploy Backstage (with TechDocs enabled by default), you get a basic out-of-the box experience. -<img data-zoomable src="../../assets/techdocs/architecture-basic.drawio.svg" alt="TechDocs Architecture diagram" /> +<!-- todo: Needs zoomable plugin --> + +![TechDocs Architecture diagram](../../assets/techdocs/architecture-basic.drawio.svg) > Note: See below for our recommended deployment architecture which takes care > of stability, scalability and speed. Also look at the @@ -42,7 +44,9 @@ storage system (e.g. AWS S3, GCS or Azure Blob Storage). Read more in This is how we recommend deploying TechDocs in a production environment. -<img data-zoomable src="../../assets/techdocs/architecture-recommended.drawio.svg" alt="TechDocs Architecture diagram" /> +<!-- todo: Needs zoomable plugin --> + +![TechDocs Architecture diagram](../../assets/techdocs/architecture-recommended.drawio.svg) The key difference in the recommended deployment approach is where the docs are built. diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 682c6a8f8f..6b2e4383b8 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -176,29 +176,28 @@ Usage: techdocs-cli publish [options] Publish generated TechDocs site to an external storage AWS S3, Google GCS, etc. Options: - --publisher-type <TYPE> (Required always) awsS3 | googleGcs | azureBlobStorage - - same as techdocs.publisher.type in Backstage - app-config.yaml - --storage-name <BUCKET/CONTAINER NAME> (Required always) In case of AWS/GCS, use the bucket - name. In case of Azure, use container name. Same as - techdocs.publisher.[TYPE].bucketName - --entity <NAMESPACE/KIND/NAME> (Required always) Entity uid separated by / in - namespace/kind/name order (case-sensitive). Example: - default/Component/myEntity - --legacyUseCaseSensitiveTripletPaths Publishes objects with cased entity triplet prefix when set (e.g. namespace/Kind/name). - Only use if your TechDocs backend is configured the same way - --azureAccountName <AZURE ACCOUNT NAME> (Required for Azure) specify when --publisher-type - azureBlobStorage - --azureAccountKey <AZURE ACCOUNT KEY> Azure Storage Account key to use for authentication. - If not specified, you must set AZURE_TENANT_ID, - AZURE_CLIENT_ID & AZURE_CLIENT_SECRET as environment - variables. - --awsRoleArn <AWS ROLE ARN> Optional AWS ARN of role to be assumed. - --awsEndpoint <AWS ENDPOINT> Optional AWS endpoint to send requests to. - --awsS3ForcePathStyle Optional AWS S3 option to force path style. - --directory <PATH> Path of the directory containing generated files to - publish (default: "./site/") - -h, --help display help for command + --publisher-type <TYPE> (Required always) awsS3 | googleGcs | azureBlobStorage | openStackSwift - same as techdocs.publisher.type in Backstage app-config.yaml + --storage-name <BUCKET/CONTAINER NAME> (Required always) In case of AWS/GCS, use the bucket name. In case of Azure, use container name. Same as + techdocs.publisher.[TYPE].bucketName + --entity <NAMESPACE/KIND/NAME> (Required always) Entity uid separated by / in namespace/kind/name order (case-sensitive). Example: default/Component/myEntity + --legacyUseCaseSensitiveTripletPaths Publishes objects with cased entity triplet prefix when set (e.g. namespace/Kind/name). Only use if your TechDocs backend is configured + the same way. (default: false) + --azureAccountName <AZURE ACCOUNT NAME> (Required for Azure) specify when --publisher-type azureBlobStorage + --azureAccountKey <AZURE ACCOUNT KEY> Azure Storage Account key to use for authentication. If not specified, you must set AZURE_TENANT_ID, AZURE_CLIENT_ID & + AZURE_CLIENT_SECRET as environment variables. + --awsRoleArn <AWS ROLE ARN> Optional AWS ARN of role to be assumed. + --awsEndpoint <AWS ENDPOINT> Optional AWS endpoint to send requests to. + --awsProxy <HTTPS Proxy> Optional Proxy to use for AWS requests. + --awsS3sse <AWS SSE> Optional AWS S3 Server Side Encryption. + --awsS3ForcePathStyle Optional AWS S3 option to force path style. + --awsBucketRootPath <AWS BUCKET ROOT PATH> Optional sub-directory to store files in Amazon S3 + --osCredentialId <OPENSTACK SWIFT APPLICATION CREDENTIAL ID> (Required for OpenStack) specify when --publisher-type openStackSwift + --osSecret <OPENSTACK SWIFT APPLICATION CREDENTIAL SECRET> (Required for OpenStack) specify when --publisher-type openStackSwift + --osAuthUrl <OPENSTACK SWIFT AUTHURL> (Required for OpenStack) specify when --publisher-type openStackSwift + --osSwiftUrl <OPENSTACK SWIFT SWIFTURL> (Required for OpenStack) specify when --publisher-type openStackSwift + --gcsBucketRootPath <GCS BUCKET ROOT PATH> Optional sub-directory to store files in Google cloud storage + --directory <PATH> Path of the directory containing generated files to publish (default: "./site/") + -h, --help display help for command ``` ### Migrate content for case-insensitive access diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 097d31f760..49ee9d72c2 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -49,9 +49,9 @@ techdocs: # will be broken in these scenarios. legacyCopyReadmeMdToIndexMd: false - # techdocs.builder can be either 'local' or 'external. + # techdocs.builder can be either 'local' or 'external'. # 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. + # techdocs-backend will try to generate the docs, publish to storage and show the generated docs afterwards. # 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. @@ -136,6 +136,11 @@ techdocs: # https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/s3clientconfig.html#endpoint endpoint: ${AWS_ENDPOINT} + # (Optional) HTTPS proxy to use for S3 Requests + # Defaults to using no proxy + # This allows docs to be published and read from behind a proxy + httpsProxy: ${HTTPS_PROXY} + # (Optional) Whether to use path style URLs when communicating with S3. # Defaults to false. # This allows providers like LocalStack, Minio and Wasabi (and possibly others) to be used to host tech docs. diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index c2070e7840..3f7edd23a0 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -89,11 +89,15 @@ const AppRoutes = () => { I know, you're curious to see how it looks, aren't you? See the image below: -<img data-zoomable src="../../assets/techdocs/report-issue-addon.png" alt="TechDocs Report Issue Add-on" /> +<!-- todo: Needs zoomable plugin --> + +![TechDocs Report Issue Add-on](../../assets/techdocs/report-issue-addon.png) By clicking the open new issue button, you will be redirected to the new issue page according to the source code provider you are using: -<img data-zoomable src="../../assets/techdocs/report-issue-template.png" alt="TechDocs Report Issue Template" /> +<!-- todo: Needs zoomable plugin --> + +![TechDocs Report Issue Template](../../assets/techdocs/report-issue-template.png) That's it! Now, we need the TechDocs Backend plugin for the frontend to work. @@ -262,12 +266,12 @@ You will have to install the `mkdocs` and `mkdocs-techdocs-core` package from pip, as well as `graphviz` and `plantuml` from your OS package manager (e.g. apt). -You can do so by including the following lines in the last step of your +You can do so by including the following lines right above `USER node` of your `Dockerfile`: ```Dockerfile RUN apt-get update && apt-get install -y python3 python3-pip -RUN pip3 install mkdocs-techdocs-core==1.0.1 +RUN pip3 install mkdocs-techdocs-core==1.1.7 ``` Please be aware that the version requirement could change, you need to check our diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index f38a806413..aaf2758241 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -230,7 +230,7 @@ If you are deploying Backstage to Amazon EC2, Amazon ECS, or Amazon EKS, you do not need to obtain the access keys separately. They can be made available in the environment automatically by defining appropriate IAM role with access to the bucket. Read more in the -[official AWS documentation for using IAM roles.](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html#use-roles). +[official AWS documentation for using IAM roles](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html#use-roles). **4b. Authentication using app-config.yaml** diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 0441d3f589..a436321fe2 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -136,9 +136,7 @@ frontend, in our tutorial it would be `http://localhost:3000`. The `Authorization callback URL` will point to the auth backend, which will most likely be `http://localhost:7007/api/auth/github/handler/frame`. -<p align='center'> - <img src='../assets/getting-started/gh-oauth.png' alt='Screenshot of the GitHub OAuth creation page' /> -</p> +![Screenshot of the GitHub OAuth creation page](../assets/getting-started/gh-oauth.png) Take note of the `Client ID` and the `Client Secret`. Open `app-config.yaml`, and add your `clientId` and `clientSecret` to this file. It should end up @@ -214,9 +212,7 @@ 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 for 7 days, it's a lucky number. -<p align='center'> - <img src='../assets/getting-started/gh-pat.png' alt='Screenshot of the GitHub Personal Access Token creation page' /> -</p> +![Screenshot of the GitHub Personal Access Token creation page](../assets/getting-started/gh-pat.png) Set the scope to your likings. For this tutorial, selecting `repo` and `workflow` is required as the scaffolding job in this guide configures a GitHub actions workflow for the newly created project. @@ -262,21 +258,18 @@ otherwise something went terribly wrong. - Register a new component, by going to `create` and choose `Register existing component` -<p align='center'> - <img data-zoomable src='../assets/getting-started/b-existing-1.png' alt='Software template main screen, with a blue button to add an existing component' /> -</p> + <!-- todo: Needs zoomable plugin --> + + ![Software template main screen, with a blue button to add an existing component](../assets/getting-started/b-existing-1.png) - As URL use `https://github.com/backstage/demo/blob/master/catalog-info.yaml`. This is used by our [demo site](https://demo.backstage.io). -<p align='center'> - <img src='../assets/getting-started/b-existing-2.png' alt='Register a new component wizard, asking for an URL to the existing component YAML file' /> -</p> -- Hit `Analyze` and review the changes. Apply them if correct + ![Register a new component wizard, asking for an URL to the existing component YAML file](../assets/getting-started/b-existing-2.png) -<p align='center'> - <img src='../assets/getting-started/b-existing-3.png' alt='Register a new component wizard, showing the metadata for the component YAML we use in this tutorial' /> -</p> +- Hit `Analyze` and review the changes. Apply them if correct. + + ![Register a new component wizard, showing the metadata for the component YAML we use in this tutorial](../assets/getting-started/b-existing-3.png) - You should receive a message that your entities have been added. - If you go back to `Home`, you should be able to find `demo`. You should be @@ -286,14 +279,12 @@ otherwise something went terribly wrong. - Go to `create` and choose to create a website with the `Example Node.js Template` - Type in a name, let's use `tutorial` and click `Next Step` -<p align='center'> - <img src='../assets/getting-started/b-scaffold-1.png' alt='Software template deployment input screen asking for a name' /> -</p> + +![Software template deployment input screen asking for a name](../assets/getting-started/b-scaffold-1.png) - You should see the following screen: -<p align='center'> - <img src='../assets/getting-started/b-scaffold-2.png' alt='Software template deployment input screen asking for the GitHub username, and name of the new repo to create' /> -</p> + +![Software template deployment input screen asking for the GitHub username, and name of the new repo to create](../assets/getting-started/b-scaffold-2.png) - For host, it should default to github.com - As owner, type your GitHub username @@ -308,7 +299,7 @@ Achievement unlocked. You've set up an installation of the core Backstage App, made it persistent, and configured it so you are now able to use software templates. -Let us know how your experience was: [on discord](https://discord.gg/EBHEGzX), +Let us know how your experience was: [on discord](https://discord.gg/backstage-687207715902193673), file issues for any [feature](https://github.com/backstage/backstage/issues/new?labels=help+wanted&template=feature_template.md) or diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index 1bfabc335c..835da0cd49 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -12,56 +12,64 @@ infrastructure needs - CI/CD, monitoring, auditing, and more. The following steps assume that you have [created a Backstage app](./create-an-app.md) and want to add an existing plugin -to it. We are using the +to it. + +We are using the [CircleCI](https://github.com/backstage/backstage/blob/master/plugins/circleci/README.md) -plugin in this example. +plugin in this example, which is designed to show CI/CD pipeline information attached +to an entity in the software catalog. 1. Add the plugin's npm package to the repo: -```bash -yarn workspace app add @backstage/plugin-circleci -``` + ```bash + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-circleci + ``` -Note the plugin is added to the `app` package, rather than the root -package.json. Backstage Apps are set up as monorepos with -[yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/). Since -CircleCI is a frontend UI plugin, it goes in `app` rather than `backend`. + Note the plugin is added to the `app` package, rather than the root + `package.json`. Backstage Apps are set up as monorepos with + [Yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/). Since + CircleCI is a frontend UI plugin, it goes in `app` rather than `backend`. 2. Add the `EntityCircleCIContent` extension to the entity pages in the app: -```diff - // packages/app/src/components/catalog/EntityPage.tsx -+import { -+ EntityCircleCIContent, -+ isCircleCIAvailable, -+} from '@backstage/plugin-circleci'; + ```diff + // packages/app/src/components/catalog/EntityPage.tsx + +import { + + EntityCircleCIContent, + + isCircleCIAvailable, + +} from '@backstage/plugin-circleci'; -... - const cicdContent = ( - <EntitySwitch> - ... -+ <EntitySwitch.Case if={isCircleCIAvailable}> -+ <EntityCircleCIContent /> -+ </EntitySwitch.Case>; - </EntitySwitch> - ); -``` + ... + const cicdContent = ( + <EntitySwitch> + ... + + <EntitySwitch.Case if={isCircleCIAvailable}> + + <EntityCircleCIContent /> + + </EntitySwitch.Case>; + </EntitySwitch> + ); + ``` -This is just one example, but each Backstage instance may integrate content or -cards to suit their needs on different pages, tabs, etc. Note that stand-alone -plugins that are not "attached" to the Software Catalog would be added outside -the `EntityPage`. + This is just one example, but each Backstage instance may integrate content or + cards to suit their needs on different pages, tabs, etc. In addition, while some + plugins such as this example are designed to annotate or support specific software + catalog entities, others may be intended to be used in a stand-alone fashion and + would be added outside the `EntityPage`, such as being added to the main navigation. -4. [Optional] Add proxy config: +3. _[Optional]_ Add a proxy config: -```yaml -// app-config.yaml -proxy: - '/circleci/api': - target: https://circleci.com/api/v1.1 - headers: - Circle-Token: ${CIRCLECI_AUTH_TOKEN} -``` + Plugins that collect data off of external services may require the use of a proxy service. + This plugin accesses the CircleCI REST API, and thus requires a proxy definition. + + ```yaml + // app-config.yaml + proxy: + '/circleci/api': + target: https://circleci.com/api/v1.1 + headers: + Circle-Token: ${CIRCLECI_AUTH_TOKEN} + ``` ### Adding a plugin page to the Sidebar diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index 05c12ffb75..535bc848fc 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -34,9 +34,7 @@ npx @backstage/create-app This will create a new Backstage App inside the current folder. The name of the app-folder is the name that was provided when prompted. -<p align='center'> - <img src='../assets/getting-started/create-app_output.png' width='600' alt='create app' /> -</p> +![create app](../assets/getting-started/create-app_output.png) Inside that directory, it will generate all the files and folder structure needed for you to run your app. diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 7db41d7768..9fa91e694f 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -82,16 +82,9 @@ const routes = ( 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. -<table> - <tr> - <th><img data-zoomable src="../assets/getting-started/sidebar-without-catalog.png" alt="Sidebar without Catalog" /></th> - <th><img data-zoomable src="../assets/getting-started/sidebar-with-catalog.png" alt="Sidebar with Catalog" /></th> - </tr> - <tr align="center"> - <td>Before</td> - <td>After</td> - </tr> -</table> +| Before | After | +| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| ![Sidebar without Catalog](../assets/getting-started/sidebar-without-catalog.png) | ![Sidebar with Catalog](../assets/getting-started/sidebar-with-catalog.png) | 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). @@ -121,7 +114,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( That's it! You should now have _(although slightly boring)_ a homepage! -<img data-zoomable src="../assets/getting-started/simple-homepage.png" alt="Screenshot of a blank homepage" /> +<!-- todo: Needs zoomable plugin --> + +![Screenshot of a blank homepage](../assets/getting-started/simple-homepage.png) In the next steps, we will make it interesting and useful! diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 962919015b..2617cfb654 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -54,9 +54,7 @@ npx @backstage/create-app@latest The wizard will ask you for the name of the app, which will also be the name of the directory -<p align='center'> - <img src='../assets/getting-started/wizard.png' alt='Screenshot of the wizard asking for a name for the app.' /> -</p> +![Screenshot of the wizard asking for a name for the app.](../assets/getting-started/wizard.png) ### Run the Backstage app @@ -69,9 +67,7 @@ cd my-backstage-app yarn dev ``` -<p align='center'> - <img src='../assets/getting-started/startup.png' alt='Screenshot of the command output, with the message web pack compiled successfully.'/> -</p> +![Screenshot of the command output, with the message web pack compiled successfully](../assets/getting-started/startup.png) It might take a little while, but as soon as the message `[0] webpack compiled successfully` appears, you can open a browser and directly @@ -80,16 +76,14 @@ You can start exploring the demo immediately. Please note that the in-memory database will be cleared when you restart the app, so you'll most likely want to carry on with the database steps. -<p align='center'> - <img src='../assets/getting-started/portal.png' alt='Screenshot of the Backstage portal.'/> -</p> +![Screenshot of the Backstage portal.](../assets/getting-started/portal.png) In the next part of this tutorial, you'll learn how to change to a persistent database, configure authentication, and add your first integration. Continue with [getting started: Configuring Backstage](configuration.md). Share your experiences, comments, or suggestions with us: -[on discord](https://discord.gg/EBHEGzX), file issues for any +[on discord](https://discord.gg/backstage-687207715902193673), file issues for any [feature](https://github.com/backstage/backstage/issues/new?labels=help+wanted&template=feature_template.md) or [plugin suggestions](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME), diff --git a/docs/getting-started/running-backstage-locally.md b/docs/getting-started/running-backstage-locally.md index e7e96316f4..d56bdf7990 100644 --- a/docs/getting-started/running-backstage-locally.md +++ b/docs/getting-started/running-backstage-locally.md @@ -86,7 +86,7 @@ option will be GitHub. To setup GitHub authentication in Backstage, see --- Congratulations! That should be it. Let us know how it went -[on discord](https://discord.gg/EBHEGzX), file issues for any +[on discord](https://discord.gg/backstage-687207715902193673), file issues for any [feature](https://github.com/backstage/backstage/issues/new?labels=help+wanted&template=feature_template.md) or [plugin suggestions](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME), diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index 748aeb2392..4a2b953bc9 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -63,7 +63,7 @@ As this provider is not one of the default providers, you will first need to ins the AWS catalog plugin: ```bash -# From the Backstage root directory +# From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-aws ``` diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index 2156f60814..6cb135900e 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -47,6 +47,11 @@ catalog: frequency: { minutes: 30 } # supports ISO duration, "human duration" as used in code timeout: { minutes: 3 } + yourSecondProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: '*' # this will match all projects + repository: '*' # this will match all repos + path: /catalog-info.yaml anotherProviderId: # another identifier organization: myorg project: myproject @@ -62,7 +67,7 @@ The parameters available are: - **`host:`** _(optional)_ Leave empty for Cloud hosted, otherwise set to your self-hosted instance host. - **`organization:`** Your Organization slug (or Collection for on-premise users). Required. -- **`project:`** Your project slug. Required. +- **`project:`** _(optional)_ Your project slug. Wildcards are supported as show on the examples above. If not set, all projects will be searched. For a project name containing spaces, use both single and double quotes as in `project: '"My Project Name"'`. - **`repository:`** _(optional)_ The repository name. Wildcards are supported as show on the examples above. If not set, all repositories will be searched. - **`path:`** _(optional)_ Where to find catalog-info.yaml files. Defaults to /catalog-info.yaml. - **`schedule`** _(optional)_: @@ -144,7 +149,7 @@ When using a custom pattern, the target is composed of five parts: - The base instance URL, `https://dev.azure.com` in this case - The organization name which is required, `myorg` in this case -- The project name which is required, `myproject` in this case +- The project name which is optional, `myproject` in this case. This defaults to \*, which scans all the projects where the token has access to. - The repository blob to scan, which accepts \* wildcard tokens and must be added after `_git/`. This can simply be `*` to scan all repositories in the project. diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index 3b3e3dd5ed..1fc476f418 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -28,3 +28,5 @@ The configuration is a structure with two elements: - `host`: The DevOps host; only `dev.azure.com` is supported. - `token` (optional): A personal access token as expected by Azure DevOps. + +> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md index 7ed05d2b3b..5d822effb5 100644 --- a/docs/integrations/gerrit/discovery.md +++ b/docs/integrations/gerrit/discovery.md @@ -17,7 +17,7 @@ As this provider is not one of the default providers, you will first need to ins the Gerrit provider plugin: ```bash -# From the Backstage root directory +# From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gerrit ``` diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index dc0b911b3d..4c448d3892 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -21,10 +21,10 @@ catalog: gitlab: yourProviderId: host: gitlab-host # Identifies one of the hosts set up in the integrations - branch: main # Optional. Uses `master` as default - group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole project will be scanned + fallbackBranch: main # Optional. Fallback to be used if there is no default branch configured at the Gitlab repository. It is only used, if `branch` is undefined. Uses `master` as default + group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` - projectPattern: /[\s\S]*/ # Optional. Filters found projects based on provided patter. Defaults to `/[\s\S]*/`, what means to not filter anything + projectPattern: '[\s\S]*' # Optional. Filters found projects based on provided patter. Defaults to `[\s\S]*`, which means to not filter anything schedule: # optional; same options as in TaskScheduleDefinition # supports cron, ISO duration, "human duration" as used in code frequency: { minutes: 30 } @@ -36,7 +36,7 @@ As this provider is not one of the default providers, you will first need to ins the gitlab catalog plugin: ```bash -# From the Backstage root directory +# From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gitlab ``` diff --git a/docs/integrations/gitlab/org.md b/docs/integrations/gitlab/org.md new file mode 100644 index 0000000000..2ab72b4bd6 --- /dev/null +++ b/docs/integrations/gitlab/org.md @@ -0,0 +1,34 @@ +--- +id: org +title: GitLab Organizational Data +sidebar_label: Org Data +description: Importing users and groups from a GitLab organization into Backstage +--- + +The Backstage catalog can be set up to ingest organizational data - users and +teams - directly from an organization in GitLab. The result +is a hierarchy of +[`User`](../../features/software-catalog/descriptor-format.md#kind-user) and +[`Group`](../../features/software-catalog/descriptor-format.md#kind-group) kind +entities that mirror your org setup. + +```yaml +integrations: + gitlab: + - host: gitlab.com + token: ${GITLAB_TOKEN} +``` + +This will query all users and groups from your gitlab installation. Depending on the size +of the Gitlab Instance, this can take some time and resources. + +The token that is used for the Organization Integration, has to be an Admin Personal Access Token (PAT). + +```yaml +catalog: + providers: + gitlab: + yourProviderId: + host: gitlab.com + orgEnabled: true +``` diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index dbd3f4ff43..f8b9a18c55 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -37,7 +37,6 @@ schedule it: ```diff // packages/backend/src/plugins/catalog.ts -+import { Duration } from 'luxon'; +import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; export default async function createPlugin( diff --git a/docs/local-dev/cli-build-system.md b/docs/local-dev/cli-build-system.md index cc364d0ca5..2eb695e3a3 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/local-dev/cli-build-system.md @@ -556,7 +556,22 @@ 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: +### Debugging Jest Tests + +For your productivity working with unit tests it's quite essential to have your debugging configured in IDE. It will help you to identify the root cause of the issue faster. + +#### IntelliJ IDEA + +1. Update Jest configuration template by: + +- Click on "Edit Configurations" on top panel +- In the modal dialog click on link "Edit configuration templates..." located in the bottom left corner. +- In "Jest package" you have to point to relative path of jest module (it will be suggested by IntelliJ), i.e. `~/proj/backstage/node_modules/jest` +- In "Jest config" point to your jest configuration file, use absolute path for that, i.e. `--config /Users/user/proj/backstage/packages/cli/config/jest.js --runInBand` + +2. Now you can run any tests by clicking on green arrow located on `describe` or `it`. + +#### VS Code ```jsonc { @@ -571,7 +586,7 @@ If you want to configure editor integration for tests we recommend executing the } ``` -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: +A complete launch configuration for VS Code debugging may look like this: ```json { @@ -583,9 +598,6 @@ If you also want to enable source maps when debugging tests, you can do so by se "disableOptimisticBPs": true, "program": "${workspaceFolder}/node_modules/.bin/jest", "cwd": "${workspaceFolder}", - "env": { - "ENABLE_SOURCE_MAPS": "true" - }, "args": [ "--config", "node_modules/@backstage/cli/config/jest.js", @@ -638,8 +650,34 @@ The following is an excerpt of a typical setup of an isomorphic library package: "files": ["dist"], ``` +## Subpath Exports + +The Backstage CLI supports implementation of subpath exports through the `"exports"` field in `package.json`. It might for example look like this: + +```json + "name": "@backstage/plugin-foo", + "exports": { + ".": "./src/index.ts", + "./components": "./src/components.ts", + }, +``` + +This in turn would allow you to import anything exported in `src/index.ts` via `@backstage/plugins-foo`, and `src/components.ts` via `@backstage/plugins-foo/components`. Note that patterns are not supported, meaning the exports may not contain `*` wildcards. + +As with the rest of the Backstage CLI build system, the setup is optimized for local development, which is why the `"exports"` targets point directly to source files. The `package build` command will detect the `"exports"` field and automatically generate the corresponding `dist` files, and the `prepublish` command will rewrite the `"exports"` field to point to the `dist` files, as well as generating folder-based entry points for backwards compatibility. + +TypeScript support is currently handled though the `typesVersions` field, as there is not yet a module resolution mode that works well with `"exports"`. You can craft the `typesVersions` yourself, but it will also be automatically generated by the `migrate package-exports` command. + +To add subpath exports to an existing package, simply add the desired `"exports"` fields and then run the following command: + +```bash +yarn backstage-cli package migrate package-exports +``` + ## Experimental Type Build +> Note: Experimental type builds are deprecated and will be removed in the future. They have been replaced by [subpath exports](#subpath-exports). + 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. diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index b43825d733..174fb3e348 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -120,7 +120,6 @@ Build a package for production deployment or publishing Options: --role <name> Run the command with an explicit package role --minify Minify the generated code. Does not apply to app or backend packages. - --experimental-type-build Enable experimental type build. Does not apply to app or backend packages. --skip-build-dependencies Skip the automatic building of local dependencies. Applies to backend packages only. --stats If bundle stats are available, write them to the output directory. Applies to app packages only. --config <path> Config files to load instead of app-config.yaml. Applies to app packages only. (default: []) @@ -355,7 +354,7 @@ package. This essentially calls `yarn pack` in each included package and unpacks the resulting archive in the target `workspace-dir`. ```text -Usage: backstage-cli build-workspace [options] <workspace-dir> +Usage: backstage-cli build-workspace [options] <workspace-dir> ``` ## create-github-app @@ -368,7 +367,7 @@ Launches a browser to create the App through GitHub and saves the result as a YAML file that can be referenced in the GitHub integration configuration. ```text -Usage: backstage-cli create-github-app <github-org> +Usage: backstage-cli create-github-app <github-org> ``` ## info diff --git a/docs/openapi/definitions/auth.yaml b/docs/openapi/definitions/auth.yaml index 863544c71a..1ed649da9c 100644 --- a/docs/openapi/definitions/auth.yaml +++ b/docs/openapi/definitions/auth.yaml @@ -19,7 +19,7 @@ info: version: 0.1.1-alpha.8 externalDocs: description: Backstage official documentation - url: https://github.com/backstage/backstage/blob/master/docs/README.md + url: https://backstage.io/docs servers: - url: http://localhost:7007/api/auth/ tags: diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index 2ac211bf97..8e3d73ded6 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -111,4 +111,4 @@ guidelines to get started. If you have specific questions about the roadmap, please create an [issue](https://github.com/backstage/backstage/issues/new/choose), ping us on -[Discord](https://discord.gg/qxsEfa8Vq8), or [book time](https://info.backstage.spotify.com/office-hours) with the Spotify team. +[Discord](https://discord.gg/backstage-687207715902193673), or [book time](https://info.backstage.spotify.com/office-hours) with the Spotify team. diff --git a/docs/overview/support.md b/docs/overview/support.md index 662a37c6ae..a0444c4d34 100644 --- a/docs/overview/support.md +++ b/docs/overview/support.md @@ -4,7 +4,7 @@ title: Support and community description: Support and Community Details and Links --- -- [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the +- [Discord chatroom](https://discord.gg/backstage-687207715902193673) - Get support or discuss the project. - [Stack Overflow](https://stackoverflow.com/questions/tagged/backstage) - Browse or ask questions on Stack Overflow. - [Good First Issues](https://github.com/backstage/backstage/contribute) - Start diff --git a/docs/overview/threat-model.md b/docs/overview/threat-model.md index 57fafd1c28..a6435f8cc5 100644 --- a/docs/overview/threat-model.md +++ b/docs/overview/threat-model.md @@ -28,6 +28,8 @@ Other responsibilities include protecting the integrity of configuration files a The integrator is ultimately responsible for auditing usage of internal and external plugins as these run on the host system and have access to configuration and secrets. When installing plugins from sources like NPM, you should vet these in the same way that you would vet any other package installed from that source. +The integrator is also responsible for maintaining the resolved NPM dependencies of their Backstage project. This involves ensuring that `yarn.lock` receives updated versions of packages that have vulnerabilities, when those fixed versions are in range of what the Backstage packages request in their respective `package.json` files. This is commonly done by employing automated tooling such as [Dependabot](https://dependabot.com/), [Snyk](https://snyk.io/), and/or [Renovate](https://docs.renovatebot.com/) on your own repository. When fixed versions exist that are _not_ in range of what Backstage packages request, or when larger operations such as switching out an entire dependency for another one is required, maintainers collaborate with contributors to try to address those dependency declarations in the main project as soon as possible. + ## Common Backend Configuration There are many common facilities that are configured centrally and available to all Backstage backend plugins. For example there is a `DatabaseManager` that provides access to a SQL database, `TaskScheduler` for scheduling long-running tasks, `Logger` as a general logging facility, and `UrlReader` for reading content from external sources. These are all configured either directly in code, or within the `backend` block of the static configuration. The appropriate care needs to be taken to ensure that any secrets remain confidential and no malicious configuration is injected. diff --git a/docs/overview/what-is-backstage.md b/docs/overview/what-is-backstage.md index 38b67e81de..f18993770a 100644 --- a/docs/overview/what-is-backstage.md +++ b/docs/overview/what-is-backstage.md @@ -5,7 +5,7 @@ title: What is Backstage? description: Backstage is an open platform for building developer portals. Powered by a centralized software catalog, Backstage restores order to your microservices and infrastructure --- -![software-catalog](https://backstage.io/blog/assets/6/header.png) +![software-catalog](../assets/header.png) [Backstage](https://backstage.io/) is an open platform for building developer portals. Powered by a centralized software catalog, Backstage restores order to @@ -61,4 +61,4 @@ Backstage is a CNCF Incubation project after graduating from Sandbox. Read the a administrators together in one place. If you have questions or want support, please join our -[Discord chatroom](https://discord.gg/EBHEGzX). +[Discord chatroom](https://discord.gg/backstage-687207715902193673). diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index d2c467ff08..e71601b703 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -47,7 +47,8 @@ The permissions framework uses a new `permission-backend` plugin to accept autho 1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend: ```bash -$ yarn workspace backend add @backstage/plugin-permission-backend +# From your Backstage root directory +$ yarn add --cwd packages/backend @backstage/plugin-permission-backend ``` 2. Add the following to a new file, `packages/backend/src/plugins/permission.ts`. This adds the permission-backend router, and configures it with a policy which allows everything. diff --git a/docs/permissions/overview.md b/docs/permissions/overview.md index b972814aeb..693f848c82 100644 --- a/docs/permissions/overview.md +++ b/docs/permissions/overview.md @@ -34,6 +34,8 @@ The permission framework was designed with a few key properties in mind: 4. An authorization decision is sent to the plugin from the permission backend. +5. The user is either granted access or an error is shown. The plugin is responsible for implementing a response to the user. + ## How do I get started? See the "[getting started](./getting-started.md)" permission documentation for Backstage integrators. diff --git a/docs/permissions/plugin-authors/01-setup.md b/docs/permissions/plugin-authors/01-setup.md index 18de410612..c368505849 100644 --- a/docs/permissions/plugin-authors/01-setup.md +++ b/docs/permissions/plugin-authors/01-setup.md @@ -26,6 +26,9 @@ The source code is available here: git checkout master -- plugins/example-todo-list/ git checkout master -- plugins/example-todo-list-backend/ git checkout master -- plugins/example-todo-list-common/ + sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json + sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json + sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done cd - ``` @@ -36,16 +39,17 @@ The source code is available here: 2. Add these packages as dependencies for your Backstage app: - ``` - $ yarn workspace backend add @internal/plugin-todo-list-backend@^1.0.0 @internal/plugin-todo-list-common@^1.0.0 - $ yarn workspace app add @internal/plugin-todo-list@^1.0.0 + ```sh + # From your Backstage root directory + $ yarn add --cwd packages/backend @internal/plugin-todo-list-backend @internal/plugin-todo-list-common + $ yarn add --cwd packages/app @internal/plugin-todo-list ``` 3. Include the backend and frontend plugin in your application: Create a new `packages/backend/src/plugins/todolist.ts` with the following content: - ```javascript + ```typescript import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { createRouter } from '@internal/plugin-todo-list-backend'; import { Router } from 'express'; diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 241b175987..44a3e43718 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -100,7 +100,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`: Pass the `permissions` object to the plugin in `packages/backend/src/plugins/todolist.ts`: ```diff - import { IdentityClient } from '@backstage/plugin-auth-backend'; + import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { createRouter } from '@internal/plugin-todo-list-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -112,7 +112,7 @@ Pass the `permissions` object to the plugin in `packages/backend/src/plugins/tod }: PluginEnvironment): Promise<Router> { return await createRouter({ logger, - identity: new IdentityClient({ + identity: DefaultIdentityClient.create({ discovery, issuer: await discovery.getExternalBaseUrl('auth'), }), @@ -146,7 +146,7 @@ In order to test the logic above, the integrators of your backstage instance nee - async handle(): Promise<PolicyDecision> { + async handle( + request: PolicyQuery, -+ user?: BackstageIdentityResponse, ++ _user?: BackstageIdentityResponse, + ): Promise<PolicyDecision> { + if (isPermission(request.permission, todoListCreatePermission)) { + return { @@ -204,7 +204,7 @@ First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`: const router = await createRouter({ logger: getVoidLogger(), identity: {} as DefaultIdentityClient, -+ permissions: toPermissionEvaluator, ++ permissions: permissionEvaluator, }); app = express().use(router); }); @@ -235,7 +235,7 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ + ServerTokenManager, } from '@backstage/backend-common'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; - import { ServerPermissionClient } from '@backstage/plugin-permission-node'; ++ import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; @@ -285,4 +285,44 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ module.hot?.accept(); ``` +Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`: + +```diff + import { loggerToWinstonLogger } from '@backstage/backend-common'; + import { + coreServices, + createBackendPlugin, + } from '@backstage/backend-plugin-api'; + import { createRouter } from './service/router'; + + /** + * The example TODO list backend plugin. + * + * @alpha + */ + export const exampleTodoListPlugin = createBackendPlugin({ + pluginId: 'exampleTodoList', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, ++ permissions: coreServices.permissions, + }, +- async init({ identity, logger, httpRouter }) { ++ async init({ identity, logger, httpRouter, permissions }) { + httpRouter.use( + await createRouter({ + identity, + logger: loggerToWinstonLogger(logger), + permissions, + }), + ); + }, + }); + }, + }); +``` + Now when you run `yarn tsc` you should have no more errors. diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index 8913b6d438..b063727c71 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -76,7 +76,7 @@ This enables decisions based on characteristics of the resource, but it's import Install the missing module: ``` -$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node +$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node zod ``` Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append the following code: @@ -89,7 +89,8 @@ import { Todo, TodoFilter } from './todos'; export const createTodoListPermissionRule = makeCreatePermissionRule< Todo, - TodoFilter + TodoFilter, + typeof TODO_LIST_RESOURCE_TYPE >(); export const isOwner = createTodoListPermissionRule({ @@ -97,8 +98,8 @@ export const isOwner = createTodoListPermissionRule({ description: 'Should allow only if the todo belongs to the user', resourceType: TODO_LIST_RESOURCE_TYPE, paramsSchema: z.object({ - userId: z.string().describe('User ID to match on the resource') - }) + userId: z.string().describe('User ID to match on the resource'), + }), apply: (resource: Todo, { userId }) => { return resource.author === userId; }, @@ -187,6 +188,7 @@ Make sure `todoListConditions` and `createTodoListConditionalDecision` are expor ```diff export * from './service/router'; + export * from './conditionExports'; + export { exampleTodoListPlugin } from './plugin'; ``` ## Test the authorized update endpoint @@ -209,7 +211,6 @@ Let's go back to the permission policy's handle function and try to authorize ou + import { + todoListCreatePermission, + todoListUpdatePermission, -+ TODO_LIST_RESOURCE_TYPE, + } from '@internal/plugin-todo-list-common'; + import { + todoListConditions, @@ -217,7 +218,11 @@ Let's go back to the permission policy's handle function and try to authorize ou + } from '@internal/plugin-todo-list-backend'; ... - + async handle( + request: PolicyQuery, +- _user?: BackstageIdentityResponse, ++ user?: BackstageIdentityResponse, + ): Promise<PolicyDecision> { if (isPermission(request.permission, todoListCreatePermission)) { return { result: AuthorizeResult.ALLOW, @@ -236,6 +241,7 @@ Let's go back to the permission policy's handle function and try to authorize ou return { result: AuthorizeResult.ALLOW, }; + } ``` For any incoming update requests, we now return a _Conditional Decision_. We are saying: diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md index 8ddcffdcd9..e84f53f176 100644 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md +++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md @@ -15,7 +15,7 @@ One possible solution may leverage the batching functionality to authorize all o - res.json(getAll()) + const items = getAll(); + const decisions = await permissions.authorize( -+ items.map(({ id }) => ({ permission: todosListRead, resourceRef: id })), ++ items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })), + ); + const filteredItems = decisions.filter( @@ -53,7 +53,7 @@ Let's add another permission to the plugin. resourceType: TODO_LIST_RESOURCE_TYPE, }); + -+ export const todosListRead = createPermission({ ++ export const todoListReadPermission = createPermission({ + name: 'todos.list.read', + attributes: { action: 'read' }, + resourceType: TODO_LIST_RESOURCE_TYPE, @@ -81,10 +81,9 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev import { todosListCreate, todosListUpdate, -+ todosListRead, ++ todoListReadPermission, TODO_LIST_RESOURCE_TYPE, } from './permissions'; -+ import { rules } from './rules'; + const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules)); @@ -95,7 +94,7 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev + ); + + const decision = ( -+ await permissions.authorizeConditional([{ permission: todosListRead }], { ++ await permissions.authorizeConditional([{ permission: todoListReadPermission }], { + token, + }) + )[0]; @@ -110,7 +109,6 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev + } else { + res.json(getAll()); + } -+ } - res.json(getAll()); }); ``` @@ -121,7 +119,7 @@ Since `TodoFilter` used in our plugin matches the structure of the conditions ob ## Test the authorized read endpoint -Let's update our permission policy to return a conditional result whenever a `todosListRead` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission. +Let's update our permission policy to return a conditional result whenever a `todoListReadPermission` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission. ```diff // packages/backend/src/plugins/permission.ts @@ -132,7 +130,6 @@ import { todoListCreatePermission, todoListUpdatePermission, + todoListReadPermission, - TODO_LIST_RESOURCE_TYPE, } from '@internal/plugin-todo-list-common'; ... diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index d494ea60ba..753ce56461 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -56,7 +56,11 @@ Let's make the following changes in `plugins/todo-list/src/components/TodoListPa - Add - </Button> + {!loadingPermission && ( -+ <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}> ++ <Button ++ disabled={!canAddTodo} ++ variant="contained" ++ onClick={() => onAdd(title.current)} ++ > + Add + </Button> + )} @@ -118,7 +122,10 @@ Providing a disabled state can be a helpful signal to users, but there may be ca - <Grid item> - <AddTodo onAdd={handleAdd} /> - </Grid> -+ <RequirePermission permission={todoListCreatePermission} errorPage={<></>}> ++ <RequirePermission ++ permission={todoListCreatePermission} ++ errorPage={<></>} ++ > + <Grid item> + <AddTodo onAdd={handleAdd} /> + </Grid> @@ -149,11 +156,15 @@ Providing a disabled state can be a helpful signal to users, but there may be ca onChange={e => (title.current = e.target.value)} /> - {!loadingPermission && ( -- <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}> +- <Button +- disabled={!canAddTodo} +- variant="contained" +- onClick={() => onAdd(title.current)} +- > - Add - </Button> - )} -+ <Button variant="contained" onClick={handleAdd}> ++ <Button variant="contained" onClick={() => onAdd(title.current)}> + Add + </Button> </Box> diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 517779d161..9ce835f5f5 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -66,8 +66,8 @@ To actually attach and run the plugin router, you will make some modifications to your backend. ```bash -# From the Backstage root directory -yarn add --cwd packages/backend @internal/plugin-carmen-backend@^0.1.1 # Change this to match the plugin's package.json +# From your Backstage root directory +yarn add --cwd packages/backend @internal/plugin-carmen-backend@^0.1.0 # Change this to match the plugin's package.json ``` Create a new file named `packages/backend/src/plugins/carmen.ts`, and add the diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index da7cf0051a..cbd789c757 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -33,7 +33,7 @@ You can also serve the plugin in isolation by running `yarn start` in the plugin directory. Or by using the yarn workspace command, for example: ```bash -yarn workspace @backstage/my-plugin start # Also supports --check +yarn workspace @backstage/plugin-my-plugin start # Also supports --check ``` This method of serving the plugin provides quicker iteration speed and a faster diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md index d9841598bb..a572143d6e 100644 --- a/docs/plugins/feature-flags.md +++ b/docs/plugins/feature-flags.md @@ -56,7 +56,7 @@ The users selection is saved in the users browsers local storage. Once toggled i The easiest way to control content based on the state of a feature flag is to use the [FeatureFlagged](https://backstage.io/docs/reference/core-app-api.featureflagged) component. ```ts -import { FeatureFlagged } from '@backstage/core-app-api' +import { FeatureFlagged } from '@backstage/core-app-api'; ... diff --git a/docs/plugins/new-backend-system.md b/docs/plugins/new-backend-system.md index bfc6f79661..2ff061a60b 100644 --- a/docs/plugins/new-backend-system.md +++ b/docs/plugins/new-backend-system.md @@ -4,11 +4,11 @@ title: New Backend System description: Details of the upcoming backend system --- -> **DISCLAIMER: The new backend system is under active development and is not considered stable** +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** ## Status -The new backend system is under active development, and only a small number of plugins have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet. +The new backend system is in alpha, and only a small number of plugins have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet. You can find an example backend setup in [the backend-next package](https://github.com/backstage/backstage/tree/master/packages/backend-next). @@ -84,7 +84,7 @@ import { // export type ExamplePluginOptions = { exampleOption: boolean }; export const examplePlugin = createBackendPlugin({ // unique id for the plugin - id: 'example', + pluginId: 'example', // It's possible to provide options to the plugin // register(env, options: ExamplePluginOptions) { register(env) { @@ -111,7 +111,7 @@ If we wanted our plugin to accept options as well, we'd accept the options as th ```ts export const examplePlugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env, options?: { silent?: boolean }) { env.registerInit({ deps: { logger: coreServices.logger }, diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index c71b27b448..8bf21bf589 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -117,5 +117,3 @@ backend-side authorization. To smooth this process out you can use proxy - either the one you already have (like Nginx, HAProxy, etc.) or the proxy-backend plugin that we provide for the Backstage backend. [Read more](https://github.com/backstage/backstage/blob/master/plugins/proxy-backend/README.md) - -[Back to Getting Started](../README.md) diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index baee47c695..2fa086c444 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -371,14 +371,4 @@ Note: wrapping in the test application **requires** you to do a `find()` or # Debugging Jest Tests -Currently, debugging Jest tests using IntelliJ or `node-debugger` is possible -but can be -[problematic to set up.](https://intellij-support.jetbrains.com/hc/en-us/community/posts/115000634564-Debugging-Jest-unit-tests) - -It is possible, but you might spend a decent amount of time configuring your -IDE. - -In most cases, we have found that using `console.log` works well. - -Note: if your console.logs are not being displayed, focus your specific unit -test from the command line by running them like so `yarn test MyTest`. +You can find it [here](https://backstage.io/docs/local-dev/cli-build-system#debugging-jest-tests) diff --git a/docs/plugins/url-reader.md b/docs/plugins/url-reader.md index 595c87ab94..fa9c341543 100644 --- a/docs/plugins/url-reader.md +++ b/docs/plugins/url-reader.md @@ -40,7 +40,7 @@ i.e.`packages/backend/src/index.ts`. ```ts // File: packages/backend/src/index.ts -import { URLReaders } from '@backstage/backend-common'; +import { UrlReaders } from '@backstage/backend-common'; function makeCreateEnv(config: Config) { // .... diff --git a/docs/releases/v1.0.0.md b/docs/releases/v1.0.0.md index 44a4f48213..6906ef8ae7 100644 --- a/docs/releases/v1.0.0.md +++ b/docs/releases/v1.0.0.md @@ -85,7 +85,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/releases/tag/v1.0.0) for this release - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) diff --git a/docs/releases/v1.1.0.md b/docs/releases/v1.1.0.md index 0417423247..daf31a536f 100644 --- a/docs/releases/v1.1.0.md +++ b/docs/releases/v1.1.0.md @@ -63,7 +63,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.1.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) diff --git a/docs/releases/v1.10.0-next.2-changelog.md b/docs/releases/v1.10.0-next.2-changelog.md index 8f1dec1e8d..4d8243799b 100644 --- a/docs/releases/v1.10.0-next.2-changelog.md +++ b/docs/releases/v1.10.0-next.2-changelog.md @@ -127,7 +127,7 @@ - e4469d0ec1: The ADR plugin can now work with sites other than GitHub. Expanded the ADR backend plugin to provide endpoints to facilitate this. - **BREAKING** The ADR plugin now uses UrlReaders. You will have to [configure integrations](https://backstage.io/docs/integrations/index#configuration) for all sites you want to get ADRs from. If you would like to create your own implementation that has different behavior, you can override the AdrApi [just like you can with other apis.](https://backstage.io/docs/api/utility-apis#app-apis) The previously used Octokit implementation has been completely removed. + **BREAKING** The ADR plugin now uses UrlReaders. You will have to [configure integrations](https://backstage.io/docs/integrations#configuration) for all sites you want to get ADRs from. If you would like to create your own implementation that has different behavior, you can override the AdrApi [just like you can with other apis.](https://backstage.io/docs/api/utility-apis#app-apis) The previously used Octokit implementation has been completely removed. ### Patch Changes @@ -424,7 +424,7 @@ - e4469d0ec1: The ADR plugin can now work with sites other than GitHub. Expanded the ADR backend plugin to provide endpoints to facilitate this. - **BREAKING** The ADR plugin now uses UrlReaders. You will have to [configure integrations](https://backstage.io/docs/integrations/index#configuration) for all sites you want to get ADRs from. If you would like to create your own implementation that has different behavior, you can override the AdrApi [just like you can with other apis.](https://backstage.io/docs/api/utility-apis#app-apis) The previously used Octokit implementation has been completely removed. + **BREAKING** The ADR plugin now uses UrlReaders. You will have to [configure integrations](https://backstage.io/docs/integrations#configuration) for all sites you want to get ADRs from. If you would like to create your own implementation that has different behavior, you can override the AdrApi [just like you can with other apis.](https://backstage.io/docs/api/utility-apis#app-apis) The previously used Octokit implementation has been completely removed. - 21ffbdd5ee: Clarify that default ADR parsers support MADR specification v2.x diff --git a/docs/releases/v1.10.0.md b/docs/releases/v1.10.0.md index 93ab357370..b06ab63747 100644 --- a/docs/releases/v1.10.0.md +++ b/docs/releases/v1.10.0.md @@ -71,7 +71,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.10.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) diff --git a/docs/releases/v1.11.0-changelog.md b/docs/releases/v1.11.0-changelog.md new file mode 100644 index 0000000000..42f9e9720b --- /dev/null +++ b/docs/releases/v1.11.0-changelog.md @@ -0,0 +1,2876 @@ +# Release v1.11.0 + +## @backstage/app-defaults@1.2.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-permission-react@0.4.10 + +## @backstage/backend-app-api@0.4.0 + +### Minor Changes + +- 01a075ec1d: **BREAKING**: Renamed `RootHttpRouterConfigureOptions` to `RootHttpRouterConfigureContext`, and removed the unused type `ServiceOrExtensionPoint`. +- 4ae71b7f2e: **BREAKING** Renaming `*Factory` exports to `*ServiceFactory` instead. For example `configFactory` now is exported as `configServiceFactory`. +- d31d8e00b3: **BREAKING** `HttpServerCertificateOptions` when specified with a `key` and `cert` should also have the `type: 'pem'` instead of `type: 'plain'` + +### Patch Changes + +- a18da2f8b5: Fixed an issue were the log redaction didn't properly escape RegExp characters. +- 5febb216fe: Updated to match the new `CacheService` interface. +- e716946103: Updated usage of the lifecycle service. +- f60cca9da1: Updated database factory to pass service deps required for restoring database state during development. +- 610d65e143: Updates to match new `BackendFeature` type. +- 725383f69d: Tweaked messaging in the README. +- b86efa2d04: Updated usage of `ServiceFactory`. +- ab22515647: The shutdown signal handlers are now installed as part of the backend instance rather than the lifecycle service, and explicitly cause the process to exit. +- b729f9f31f: Moved the options of the `config` and `rootHttpRouter` services out to the factories themselves, where they belong +- ed8b5967d7: `HttpRouterFactoryOptions.getPath` is now optional as a default value is always provided in the factory. +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/backend-tasks@0.4.3 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-node@0.7.5 + +## @backstage/backend-dev-utils@0.1.0 + +### Minor Changes + +- 9d9cdea117: Introduced a new package for backend development utilities. Similar to how `@backstage/dev-utils` is used in the frontend. + +### Patch Changes + +- 725383f69d: Tweaked messaging in the README. + +## @backstage/backend-plugin-api@0.4.0 + +### Minor Changes + +- e716946103: **BREAKING**: Split out the hook for both lifecycle services so that the first parameter of `addShutdownHook` is the hook function, and the second is the options. +- 0ff03319be: **BREAKING**: The plugin ID option passed to `createBackendPlugin` is now `pluginId`, rather than just `id`. This is to make it match `createBackendModule` more closely. +- 71a5ec0f06: **BREAKING**: Switched out `LogMeta` type for `JsonObject`. +- 5febb216fe: **BREAKING**: The `CacheService` has been changed to remove the indirection of `getClient`, instead making the `CacheClient` methods directly available on the `CacheService`. In order to allow for the creation of clients with default options, there is now a new `.withOptions` method that must be implemented as part of the service interface. +- b86efa2d04: Switch `ServiceFactory` to be an opaque type, keeping only the `service` field as public API, but also adding a type parameter for the service scope. +- 610d65e143: Switched `BackendFeature` to be an opaque type. + +### Patch Changes + +- 9c9456fd33: Removed the unused `TypesToServiceRef` type +- 181c03edb5: Aligned opaque type markers to all use a `$type` property with namespacing. +- 725383f69d: Tweaked messaging in the README. +- ae88f61e00: The `register` methods passed to `createBackendPlugin` and `createBackendModule` + now have dedicated `BackendPluginRegistrationPoints` and + `BackendModuleRegistrationPoints` arguments, respectively. This lets us make it + clear on a type level that it's not possible to pass in extension points as + dependencies to plugins (should only ever be done for modules). This has no + practical effect on code that was already well behaved. +- Updated dependencies + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/catalog-model@1.2.0 + +### Minor Changes + +- d51668882d: Adds the tRPC API type to the catalog. + +### Patch Changes + +- aece6c57d2: Add additional validation for location references. +- 2518ef5b8a: Add deprecation tag to kubernetes constants in catalog, constants are now defined in plugin-kubernetes-common +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/core-app-api@1.5.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- dff4d8ddb1: Fixed an issue where an explicit port the frontend base URL could break the app. +- Updated dependencies + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + +## @backstage/core-plugin-api@1.4.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + +## @backstage/eslint-plugin@0.1.0 + +### Minor Changes + +- 179d301518: Added a new ESLint plugin with common rules for Backstage projects. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for more details. + +## @backstage/plugin-adr@0.4.0 + +### Minor Changes + +- 0a32911d8a: Display title, status and date in ADR navigation, sourced from ADR content and reverse order. +- ec34b535c0: The `AdrSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- ec34b535c0: Support displaying an icon on `AdrSearchResultListItem` +- 496cf6aff0: Render the common `<MissingAnnotationEmptyState />` component when the `backstage.io/adr-location` annotation is missing from the component +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-adr-common@0.2.6 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-adr-backend@0.3.0 + +### Minor Changes + +- 0a32911d8a: Display title, status and date in ADR navigation, sourced from ADR content and reverse order. + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.6 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-api-docs@0.9.0 + +### Minor Changes + +- 9ed53218a5: Adds a new tRPC API definition widget which users can utilize to integrate their tRPC API definitions into Backstage. + +### Patch Changes + +- 60996f8aba: Updating the readme with better `csp` instructions +- 6856ab98f4: Updated dependency `@asyncapi/react-component` to `1.0.0-next.47`. +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-auth-backend@0.18.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + +## @backstage/plugin-catalog@1.8.0 + +### Minor Changes + +- 0c1fc3986c: Added Markdown support in the `AboutCard` description section +- 0eaa579f89: The `CatalogSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- d7f55f6bd2: The list of entities takes into account the title when its different from the name to sort the entities. + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- 7d46ca2f68: Make catalog search result list a bit more customizable + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.10 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.2.0 + +### Minor Changes + +- 1ba120faa3: Added new mechanism to handle deltas in incremental providers + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 407dc01fc9: Removing extra imports for `run` script as `TestBackend` auto loads the default factories +- b7e36660d5: Return `EventSubscriber` from `addIncrementalEntityProvider` to hook up to `EventsBackend` +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- 77c41b6924: Updated README to include newer API options for incremental entity providers +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.0 + +### Minor Changes + +- fb568e2683: Improve performance when loading users via group membership. + Users data is now loaded from a paged query, rather than having to make an extra call per user to load each user's profiles. + + Note, there are still additional per user calls made to load user avatars + +### Patch Changes + +- 4c86436fdf: Fix MS Graph provider to use target URL for fetching access token +- 0daa328c3a: Extract default transformers to their own file +- 28f9883440: Fixed a bug reading the `user.select` field expected from the `app-config.yaml` configuration +- c5b119ad9c: Increased default page size to 999 (from 100) to reduce the number of calls made to the Microsoft Graph API. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + +## @backstage/plugin-catalog-react@1.3.0 + +### Minor Changes + +- 929e1afe1b: Add `initialFilter` prop to EntityLifecyclePicker. This allows you to set an initial lifecycle for the catalog. +- fab93c2fe8: Aligned buttons on "Unregister entity" dialog to keep them on the same line + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.10 + +## @backstage/plugin-entity-feedback@0.1.0 + +### Minor Changes + +- a3c86a7ed2: Implement entity feedback plugin, check out the `README.md` for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/plugin-entity-feedback-common@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-entity-feedback-backend@0.1.0 + +### Minor Changes + +- a3c86a7ed2: Implement entity feedback plugin, check out the `README.md` for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-entity-feedback-common@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/plugin-auth-node@0.2.11 + +## @backstage/plugin-entity-feedback-common@0.1.0 + +### Minor Changes + +- a3c86a7ed2: Implement entity feedback plugin, check out the `README.md` for more details! + +## @backstage/plugin-entity-validation@0.1.0 + +### Minor Changes + +- d34b3267f9: First implementation for the entity validation UI. For more information refer to its `README.md`. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-common@1.0.11 + +## @backstage/plugin-explore@0.4.0 + +### Minor Changes + +- 0eaa579f89: The `ToolSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.26 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-graphql-voyager@0.1.0 + +### Minor Changes + +- d1fb4b7bf1: Add the new GraphQL Voyager tool as a plugin for backstage, expose necessary API and components needed for it. + + For more information on how to use this plugin and configure it, please navigate to the [README](https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager/README.md) of the plugin. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-kubernetes-common@0.6.0 + +### Minor Changes + +- 1728c1ef01: **BREAKING**: Renamed misspelled `LimitRangeFetchReponse` to `LimitRangeFetchResponse`. + +### Patch Changes + +- 2518ef5b8a: New K8s catalog entity annotations added that will replace now deprecated k8s annotations in the catalog-model package. K8s annotation imports should now be made from plugin-kubernetes-common. +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/catalog-model@1.2.0 + +## @backstage/plugin-lighthouse@0.4.0 + +### Minor Changes + +- eef62546ce: Require @backstage/plugin-lighthouse-common package where API implementation moved to. + + **BREAKING**: the following types have been moved to @backstage/plugin-lighthouse-common: + + - `Audit` + - `AuditBase` + - `AuditCompleted` + - `AuditFailed` + - `AuditRunning` + - `FetchError` + - `LASListRequest` + - `LASListResponse` + - `LighthouseApi` + - `LighthouseCategoryAbbr` + - `LighthouseCategoryId` + - `LighthouseConfigSettings` + - `LighthouseRestApi` + - `TriggerAuditPayload` + - `Website` + - `WebsiteListResponse` + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 4bdd6bcb3c: Prefer Link over plain anchors +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/plugin-lighthouse-common@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-lighthouse-backend@0.1.0 + +### Minor Changes + +- eef62546ce: Introduce Lighthouse Backend Plugin to run scheduled Lighthouse Audits + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-lighthouse-common@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-lighthouse-common@0.1.0 + +### Minor Changes + +- eef62546ce: Introduce @backstage/plugin-lighthouse-common with the API implementation + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + +## @backstage/plugin-linguist@0.1.0 + +### Minor Changes + +- 75cfee5688: Introduced the Linguist plugin, checkout the plugin's `README.md` for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-linguist-common@0.1.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-linguist-backend@0.1.0 + +### Minor Changes + +- 75cfee5688: Introduced the Linguist plugin, checkout the plugin's `README.md` for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-linguist-common@0.1.0 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + +## @backstage/plugin-linguist-common@0.1.0 + +### Minor Changes + +- 75cfee5688: Introduced the Linguist plugin, checkout the plugin's `README.md` for more details! + +## @backstage/plugin-microsoft-calendar@0.1.0 + +### Minor Changes + +- 44412375bb: Created a new plugin `@backstage/plugin-microsoft-calendar` to display events from a Microsoft Calendar. + + please refer to the [README.md](https://github.com/backstage/backstage/blob/master/plugins/microsoft-calendar/README.md) for step-by-step instructions to setup the plugin in your Backstage instance. + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-scaffolder@1.11.0 + +### Minor Changes + +- d6bb12a2e7: - **Deprecation** - Deprecated the following exports, please import them directly from `@backstage/plugin-scaffolder-react` instead + + createScaffolderLayout + ScaffolderLayouts + LayoutOptions + LayoutTemplate + +- a521379688: Migrating the `TemplateEditorPage` to work with the new components from `@backstage/plugin-scaffolder-react` + +- 074f7e81b5: Added a missing validator check for items in an array + +- 8c2966536b: Embed scaffolder workflow in other components + +### Patch Changes + +- 04f717a8e1: `scaffolder/next`: bump `react-jsonschema-form` libraries to `v5-stable` +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- cbab8ac107: lock versions of `@rjsf/*-beta` packages +- 346d6b6630: Upgrade `@rjsf` version 5 dependencies to `beta.18` +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- d2ddde2108: Add `ScaffolderLayouts` to `NextScaffolderPage` +- 0f0da2f256: Prefer schema ordering of template properties during review content generation. +- 38992bdbaf: Fixed bug in review step refactor that caused schema-based display settings for individual property values to be discarded. +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-react@0.4.10 + - @backstage/plugin-scaffolder-common@1.2.5 + +## @backstage/plugin-scaffolder-backend@1.11.0 + +### Minor Changes + +- 0b2952ee4b: Added the option to overwrite files in the `targetPath` of the `template:fetch` action +- 127154930f: Renamed the export `scaffolderCatalogModule` to `catalogModuleTemplateKind` in order to follow the new recommended naming patterns of backend system items. This is technically a breaking change but in an alpha export, so take care to change your imports if you have already migrated to the new backend system. + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. + +- ad3edc402d: **Deprecations**: The following are deprecated and should instead be imported from the new package `@backstage/plugin-scaffolder-node`: + + - `ActionContext` + - `createTemplateAction` + - `TaskSecrets` + - `TemplateAction` + +- 6c70919f1a: Provide better error messaging when GitHub fails due to missing team definitions + +- 66cf22fdc4: Updated dependency `esbuild` to `^0.17.0`. + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-scaffolder-common@1.2.5 + +## @backstage/plugin-scaffolder-node@0.1.0 + +### Minor Changes + +- d72866f0cc: New package that takes over some of the types and functionality from `@backstage/plugin-scaffolder-backend` that are shared with other modules + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/catalog-model@1.2.0 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5 + +## @backstage/plugin-scaffolder-react@1.1.0 + +### Minor Changes + +- a07750745b: Added `DescriptionField` field override to the `next/scaffolder` +- a521379688: Migrating the `TemplateEditorPage` to work with the new components from `@backstage/plugin-scaffolder-react` +- 8c2966536b: Embed scaffolder workflow in other components +- 5555e17313: refactor `createAsyncValidators` to be recursive to ensure validators are called in nested schemas. + +### Patch Changes + +- 04f717a8e1: `scaffolder/next`: bump `react-jsonschema-form` libraries to `v5-stable` +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- cbab8ac107: lock versions of `@rjsf/*-beta` packages +- 346d6b6630: Upgrade `@rjsf` version 5 dependencies to `beta.18` +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- d2ddde2108: Add `ScaffolderLayouts` to `NextScaffolderPage` +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-scaffolder-common@1.2.5 + +## @backstage/plugin-search@1.1.0 + +### Minor Changes + +- 0eaa579f89: Update `SearchModal` component to use `SearchResult` extensions. + +### Patch Changes + +- 26fb21aa8b: Implement a `showCounts` option to display result counts per type in `SearchType.Accordion` +- 66e2aab4c4: Updated colors for each tab used on search modal, to improve color contrast. Aria label added to tabs wrapper component. `disableRipple` property on the `Tab` component removed to improve keyboard navigation indicator. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-react@1.5.0 + +### Minor Changes + +- 0eaa579f89: - Create the search results extensions, for more details see the documentation [here](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions); + - Update the `SearchResult`, `SearchResultList` and `SearchResultGroup` components to use extensions and default their props to optionally accept a query, when the query is not passed, the component tries to get it from the search context. + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-sentry@0.5.0 + +### Minor Changes + +- 6a8d6b9a0c: Add option to filter issues based on the time it was triggered + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-tech-insights-node@0.4.0 + +### Minor Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-techdocs@1.5.0 + +### Minor Changes + +- 20840b36b4: Update DocsTable and EntityListDocsTable to accept overrides for Material Table options. +- 0eaa579f89: The `TechDocsSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- c8e09cc383: Fixed bug in Techdocs reader where a techdocs page with a hash in the URL did not always jump to the document anchor. + +- cad5607411: Improve view: remove footer overlay on large screen + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- 4660b63947: Create a TechDocs `<LightBox/>` addon that allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. + + Here's an example on how to use it in a Backstage app: + + ```diff + import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, + } from '@backstage/plugin-techdocs'; + import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha'; + +import { LightBox } from '@backstage/plugin-techdocs-module-addons-contrib'; + + const AppRoutes = () => { + <FlatRoutes> + // other plugin routes + <Route path="/docs" element={<TechDocsIndexPage />}> + <DefaultTechDocsHome /> + </Route> + <Route + path="/docs/:namespace/:kind/:name/*" + element={<TechDocsReaderPage />} + > + <TechDocsAddons> + + <LightBox /> + </TechDocsAddons> + </Route> + </FlatRoutes>; + }; + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-react@1.1.3 + +## @backstage/plugin-techdocs-node@1.5.0 + +### Minor Changes + +- dd1e37649f: Deprecated getMkDocsYml in favor of getMkdocsYml (lowercase 'd') + +### Patch Changes + +- 87ab76e55c: Fixed bug caused by recent migration to AWS SDK V3 for techdocs-node. Instead of s3ForcePathStyle, forcePathStyle should be passed. +- 339d9a5b5c: Added support for using a default `mkdocs.yml` configuration file when none is provided +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-user-settings@0.7.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- c4940b6322: Added a message to advise a page reload when toggling feature flags +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/backend-common@0.18.2 + +### Patch Changes + +- 5febb216fe: **BREAKING**: The `CacheClient` interface must now also implement the `withOptions` method. The `.get()` method has also received a type parameter that helps ensure that `undefined` in the event of a cache miss is handled. + + Added a `cacheToPluginCacheManager` helper that converts a `CacheService` into a legacy `PluginCacheManager` instance. + +- 5febb216fe: Updated to match the new `CacheService` interface. + +- e716946103: Updated usage of the lifecycle service. + +- d31d8e00b3: Updated to work with the new `type: 'pem'` with `createHttpServer` from `@backstage/backend-app-api` + +- 0ff03319be: Updated usage of `createBackendPlugin`. + +- f60cca9da1: The `DatabaseManager.forPlugin` method now accepts additional service dependencies. There is no need to update existing code to pass these dependencies. + +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. + +- Updated dependencies + - @backstage/backend-app-api@0.4.0 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-dev-utils@0.1.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/backend-defaults@0.1.7 + +### Patch Changes + +- 725383f69d: Tweaked messaging in the README. +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- Updated dependencies + - @backstage/backend-app-api@0.4.0 + - @backstage/backend-plugin-api@0.4.0 + +## @backstage/backend-tasks@0.4.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/backend-test-utils@0.1.34 + +### Patch Changes + +- baf6e4c96a: Removed unnecessary `@backstage/cli` dependency. +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 1835311713: Added explicit return type signature for `mockServices.config()`. +- e716946103: Updated usage of the lifecycle service. +- 7e7557a2be: Updated the `mockServices.rootLogger` options to accept a single level option instead. +- 610d65e143: Updates to match new `BackendFeature` type. +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- b86efa2d04: Updated usage of `ServiceFactory`. +- 17b573e4be: The new backend system testing utilities have now been marked as stable API. +- f1adb2e36b: Removed the `ref` from all `mockServices`. +- d0901c9ba4: All mock service factories and mock service implementations are now available via the new experimental `mockServices` export. +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-app-api@0.4.0 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + +## @backstage/catalog-client@1.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/errors@1.1.4 + +## @backstage/cli@0.22.2 + +### Patch Changes + +- 561df21ea3: The `backstage-cli repo test` command now sets a default Jest `--workerIdleMemoryLimit` of 1GB. If needed to ensure that tests are not run in band, `--maxWorkers=2` is set as well. This is the recommended workaround for dealing with Jest workers leaking memory and eventually hitting the heap limit. +- 2815981057: Show module name causing error during build +- dd8a9afe66: Replaced several monorepo lint rules with new rules from `@backstage/eslint-plugin`. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for a full list of rules. +- a9c50af76b: Fixed Webpack loader patterns so that they properly match the file suffix only. +- 66cf22fdc4: Updated dependency `esbuild` to `^0.17.0`. +- 6d3abfded1: Switch to inline source maps for test transpilation, simplifying editor setups. +- 90616df9a8: Added an experimental mode for the `package start` command for backend packages. Enabled by setting `EXPERIMENTAL_BACKEND_START`. +- Updated dependencies + - @backstage/eslint-plugin@0.1.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + +## @backstage/codemods@0.1.43 + +### Patch Changes + +- 02f1316e57: Moved `commander` to being a regular dependency. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## @backstage/core-components@0.12.4 + +### Patch Changes + +- 68ce7d0417: Added aria labels on the support button and sidebar +- 5637ebed92: Added a global override for `window.open` that helps prevent security vulnerabilities. +- 910015f5b7: The Button component has been deprecated in favor of the LinkButton component +- 20840b36b4: Adds new type, TableOptions, extending Material Table Options. +- e81a6e0ab5: Updated Link URL validation to be more strict. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 66e2aab4c4: Navigation items in mobile sidebar now have aria label. +- Updated dependencies + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/version-bridge@1.0.3 + +## @backstage/create-app@0.4.37 + +### Patch Changes + +- 12a7b316ee: Bumped create-app version. +- 86a8dfd7b0: Added a check to ensure that Yarn v1 is used when creating new projects. +- 0eaa579f89: Update `SearchPage` template to use `SearchResult` extensions. +- 02f1316e57: Updated `packages/app/cypress/.eslintrc.json` to remove the unnecessary `import/no-extraneous-dependencies` rule. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## @backstage/dev-utils@1.0.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/app-defaults@1.2.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/integration-react@1.1.10 + - @backstage/test-utils@1.2.5 + +## @backstage/integration-react@1.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + +## @backstage/repo-tools@0.1.2 + +### Patch Changes + +- ff63acf30a: Packages without a declared `backstage.role` are now ignored. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + +## @techdocs/cli@1.3.2 + +### Patch Changes + +- dd1e37649f: Deprecated getMkDocsYml in favor of getMkdocsYml (lowercase 'd') + +- dcacf94912: Fix proxying to mkdocs + + The domain localhost may point to both 127.0.0.1 and ::1, ipv4 and ipv6 + and when node tries to lookup localhost it might prefer ipv6 while mkdocs + is only listening on ipv4. This tells node-proxy to target the ipv4 address + instead of relying on localhost hostname lookup. + +- 339d9a5b5c: Added support for using a default `mkdocs.yml` configuration file when none is provided + +- 6e0b6a0d50: Fixed publish command missing awsBucketRootPath option. + Fixed publish command having the gcsBucketRootPath option misconfigured, previously returning a boolean vs a string. + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-techdocs-node@1.5.0 + - @backstage/catalog-model@1.2.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + +## @backstage/test-utils@1.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.10 + +## @backstage/theme@0.2.17 + +### Patch Changes + +- b7705e176c: Use same table header color as @backstage/core-components Table to meet accessibility color contrast requirements. This change affects material-ui tables. + +## @backstage/plugin-adr-common@0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/integration@1.4.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-airbrake@0.3.15 + +### Patch Changes + +- 41377156d0: Adds a boolean helper function to airbrake plugin for use on the EntityPage to show/hide airbrake tab depending on whether the entity's `catalog-info.yaml` has an airbrake id set in the metadata +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/dev-utils@1.0.12 + - @backstage/test-utils@1.2.5 + +## @backstage/plugin-airbrake-backend@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-allure@0.1.31 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-analytics-module-ga@0.1.26 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-apache-airflow@0.2.8 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 4bdd6bcb3c: Prefer Link over plain anchors +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-api-docs-module-protoc-gen-doc@0.1.1 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend + +## @backstage/plugin-apollo-explorer@0.1.8 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-app-backend@0.3.42 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + +## @backstage/plugin-auth-node@0.2.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-azure-devops@0.2.6 + +### Patch Changes + +- 347b16ecea: Fixed `AssignedToCurrentUsersTeams` & `CreatedByCurrentUsersTeams` filter in `AzurePullRequestsPage` component. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-devops-backend@0.3.21 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- cc926a59bd: Fixed a bug where the azure devops host in URLs on the readme card was being URL encoded, breaking hosts with ports. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.4 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 98a00a09d5: Fixed `AzureSites` plugin start and stop issue #15904 + Fixed `AzureSites` UI load issue #15907 +- 4bdd6bcb3c: Prefer Link over plain anchors +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-azure-sites-backend@0.1.4 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.39 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-badges-backend@0.1.36 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-bazaar@0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/cli@0.22.2 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + +## @backstage/plugin-bazaar-backend@0.2.5 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + +## @backstage/plugin-bitrise@0.1.42 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-catalog-backend@1.7.2 + +### Patch Changes + +- 071354eb7d: Add additional validation as security precations for output entities. +- b977c2e69f: Minor improvements to the descriptions provided with permission rules schemas +- 2380506364: The process of adding or modifying fields in the software-catalog search index has been simplified. For more details, see [how to customize fields in the Software Catalog index](https://backstage.io/docs/features/search/how-to-guides#how-to-customize-fields-in-the-software-catalog-index). +- 9573651919: The previous migration that adds the `search.original_value` column may leave some of the entities not updated. Add a migration script to trigger a reprocessing of the entities. +- 9f71a2fd20: Location rule target patterns now also match hidden files, i.e. path components with a leading dot. +- e716946103: Updated usage of the lifecycle service. +- 1aec041c34: Fixed an issue where entities sometimes were not properly deleted during a full mutation. +- 0ff03319be: Updated usage of `createBackendPlugin`. +- fc73f6aae5: Switched the order of reprocessing statements retroactively in migrations. This only improves the experience for those who at a later time perform a large upgrade of an old Backstage installation. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + - @backstage/plugin-scaffolder-common@1.2.5 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-backend-module-aws@0.1.15 + +### Patch Changes + +- 671c6af700: Update to import `LocationSpec` from the correct package. +- 8e025f1347: Added support for `externalId` when assuming role in `AwsS3EntityProvider` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-azure@0.1.13 + +### Patch Changes + +- 2890f47517: This will add the ability to use Azure DevOps with multi project with a single value which is a new feature as previously this had to be done manually for each project that you wanted to add. + + Right now you would have to fill in multiple values in the config to use multiple projects: + + yourFirstProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: 'firstProject' # this will match the firstProject project + repository: '*' # this will match all repos + path: /catalog-info.yaml + yourSecondProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: 'secondProject' # this will match the secondProject project + repository: '*' # this will match all repos + path: /catalog-info.yaml + + With this change you can actually have all projects available where your PAT determines which you have access to, so that includes multiple projects: + + yourFirstProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: '*' # this will match all projects where your PAT has access to + repository: '*' # this will match all repos + path: /catalog-info.yaml + +- 85b04f659a: Internal refactor to not use deprecated `substr` + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.9 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + - @backstage/plugin-catalog-common@1.0.11 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-catalog-backend-module-github@0.2.5 + +### Patch Changes + +- 66158754b4: Add support for filtering out forks +- 80afd5ff8b: Use github team slug and login annotations rather than entity name to infer location annotations +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.13 + +### Patch Changes + +- 49948f644f: The config now consistently uses the term 'instance' to refer to a single GitLab API host. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 52c5685ceb: Implement Group and User Catalog Provider +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/catalog-model@1.2.0 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-common@1.0.11 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-graph@0.2.27 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- fe19058b74: Fix #16245: The CatalogGraphCard has parameter naming errors for CatalogGraphPage +- 1827710136: Expose additional props on the `CatalogGraphCard` to allow for custom node & label rendering or kind/relation filtering. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + +## @backstage/plugin-catalog-graphql@0.3.18 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-import@0.9.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-catalog-common@1.0.11 + +## @backstage/plugin-catalog-node@1.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + +## @backstage/plugin-cicd-statistics@0.1.17 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cicd-statistics@0.1.17 + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-circleci@0.3.15 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 1616e06047: Making workflow a link +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-cloudbuild@0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-code-climate@0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-code-coverage@0.2.8 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-code-coverage-backend@0.2.8 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-codescene@0.1.10 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-config-schema@0.1.38 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-cost-insights@0.12.4 + +### Patch Changes + +- dfa0bb737e: Fix bug in EntityCostsCard if cost aggregation array is empty. Allow title override on the Cost Insights Page +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-dynatrace@2.0.0 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-events-backend@0.2.3 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/plugin-events-node@0.2.3 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-module-aws-sqs@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/plugin-events-node@0.2.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-events-backend-module-azure@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + +## @backstage/plugin-events-backend-module-gerrit@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + +## @backstage/plugin-events-backend-module-github@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-module-gitlab@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-test-utils@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3 + +## @backstage/plugin-events-node@0.2.3 + +### Patch Changes + +- 19d4abf72c: Make `EventParams` typed for implementing tidier event handling. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + +## @backstage/plugin-explore-backend@0.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-explore-react@0.0.26 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-explore-common@0.0.1 + +## @backstage/plugin-firehydrant@0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-fossa@0.2.47 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-gcalendar@0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-gcp-projects@0.3.34 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-git-release-manager@0.3.28 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/integration@1.4.2 + +## @backstage/plugin-github-actions@0.5.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/integration@1.4.2 + +## @backstage/plugin-github-deployments@0.1.46 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + +## @backstage/plugin-github-issues@0.2.4 + +### Patch Changes + +- 3f88ae9d0a: Updated README.md examples to use correct components and fixed some syntax errors. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-github-pull-requests-board@0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/integration@1.4.2 + +## @backstage/plugin-gitops-profiles@0.3.33 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-gocd@0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-graphiql@0.2.47 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-graphql-backend@0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.18 + +## @backstage/plugin-home@0.4.31 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 3d1d867d42: Fixed regression that caused the `WelcomeTitle` to not be the right size when passed to the `title` property of the `<Header>` component. A Storybook entry was also added for the `WelcomeTitle` +- c553a625d2: remove unused plugin-stack-overflow dependency +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-ilert@0.2.4 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-jenkins@0.7.14 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-jenkins-common@0.1.13 + +## @backstage/plugin-jenkins-backend@0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-jenkins-common@0.1.13 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-jenkins-common@0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-kafka@0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-kafka-backend@0.2.35 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-kubernetes@0.7.8 + +### Patch Changes + +- 145a79a15b: Condenses kubernetes ui plugin to fit more onscreen and increase visibility +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.0 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-kubernetes-backend@0.9.3 + +### Patch Changes + +- 2518ef5b8a: Adding new Cluster detail fields to catalogClusterLocator. Replace deprecated imports with k8s annotations from plugin-kubernetes-common. +- 7ff81f7692: Kubernetes proxy endpoint now accepts content types that are not json +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- a53d06afe5: The name of the header used to specify a cluster to the proxy endpoint is now visible in the API reference. +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + +## @backstage/plugin-newrelic@0.3.33 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-newrelic-dashboard@0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-org@0.6.5 + +### Patch Changes + +- 5a4fa8859b: Updated `LinksGroup` to use `Link` over `ListItem` as this makes the links more obvious and follows the pattern already used in the `GroupProfileCard`. Also updated the `GroupProfileCard` `ExtraDetails` story in Storybook to enable the `showLinks` feature with this off there is no difference between it and the `default` story. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-org-react@0.1.4 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + +## @backstage/plugin-pagerduty@0.5.8 + +### Patch Changes + +- 804746dd58: The PagerDutyCard now supports an optional `readOnly` property (`<PagerDutyCard readOnly />`) for suppressing the rendering of the "Create Incident" button from the Backstage UI. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-periskop@0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-periskop-backend@0.1.13 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-permission-backend@0.5.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + +## @backstage/plugin-permission-node@0.7.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-permission-react@0.4.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-playlist@0.1.6 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.10 + - @backstage/plugin-playlist-common@0.1.4 + +## @backstage/plugin-playlist-backend@0.2.5 + +### Patch Changes + +- b977c2e69f: Minor improvements to the descriptions provided with permission rules schemas +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + - @backstage/plugin-playlist-common@0.1.4 + +## @backstage/plugin-proxy-backend@0.2.36 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-rollbar@0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-rollbar-backend@0.1.39 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.17 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/plugin-scaffolder-backend@1.11.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.10 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/plugin-scaffolder-backend@1.11.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.2 + +### Patch Changes + +- da418c89e4: Fix broken module exports and dependencies to match a backend module, rather than a frontend plugin. +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.15 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-common@1.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/types@1.0.2 + +## @backstage/plugin-search-backend@1.2.3 + +### Patch Changes + +- 1b30c9a0f7: Change the router's response to include the error message instead of its object type in case it fails during a search query because the messages have more info. +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + - @backstage/plugin-search-backend-node@1.1.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.1.3 + +### Patch Changes + +- 896566906b: Export `decodeElasticSearchPageCursor` utility function +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-pg@0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-node@1.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-shortcuts@0.3.7 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/types@1.0.2 + +## @backstage/plugin-sonarqube@0.6.3 + +### Patch Changes + +- 6310eacc11: Additional export added in order to bind SonarQubeClient to its apiref +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-sonarqube-react@0.1.2 + +## @backstage/plugin-sonarqube-backend@0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-sonarqube-react@0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-splunk-on-call@0.4.4 + +### Patch Changes + +- 3598136ac7: Refactor plugin Card component to not rerender contents unnecessarily. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @backstage/plugin-stack-overflow@0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/plugin-home@0.4.31 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-stack-overflow-backend@0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-tech-insights@0.3.7 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-tech-insights-backend@0.5.8 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/plugin-tech-insights-node@0.4.0 + - @backstage/catalog-model@1.2.0 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26 + +### Patch Changes + +- d6b912f963: Surface the cause of the json rules engine +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/plugin-tech-insights-node@0.4.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-tech-insights-common@0.2.10 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/types@1.0.2 + +## @backstage/plugin-tech-radar@0.6.1 + +### Patch Changes + +- acf6c8c10b: Deprecate `RadarEntry.url` - use `RadarEntry.links` instead + + ```diff + - url: 'https://www.javascript.com/', + key: 'javascript', + id: 'javascript', + title: 'JavaScript', + quadrant: 'languages', + links: [ + + { + + url: 'https://www.javascript.com/', + + title: 'Learn more', + + }, + ], + ``` + +- 18024a231c: Allow to set additional links to the entry description. + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.10 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/plugin-techdocs@1.5.0 + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/integration-react@1.1.10 + - @backstage/test-utils@1.2.5 + - @backstage/plugin-techdocs-react@1.1.3 + +## @backstage/plugin-techdocs-backend@1.5.3 + +### Patch Changes + +- 6f097023fc: Keep the tech docs sync event stream alive even if it is taking a while to build. +- 339d9a5b5c: Added support for using a default `mkdocs.yml` configuration file when none is provided +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-techdocs-node@1.5.0 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.10 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages + +- 4660b63947: Create a TechDocs `<LightBox/>` addon that allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. + + Here's an example on how to use it in a Backstage app: + + ```diff + import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, + } from '@backstage/plugin-techdocs'; + import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha'; + +import { LightBox } from '@backstage/plugin-techdocs-module-addons-contrib'; + + const AppRoutes = () => { + <FlatRoutes> + // other plugin routes + <Route path="/docs" element={<TechDocsIndexPage />}> + <DefaultTechDocsHome /> + </Route> + <Route + path="/docs/:namespace/:kind/:name/*" + element={<TechDocsReaderPage />} + > + <TechDocsAddons> + + <LightBox /> + </TechDocsAddons> + </Route> + </FlatRoutes>; + }; + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-techdocs-react@1.1.3 + +## @backstage/plugin-techdocs-react@1.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/version-bridge@1.0.3 + +## @backstage/plugin-todo@0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-todo-backend@0.1.39 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-user-settings-backend@0.1.6 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + +## @backstage/plugin-vault@0.1.9 + +### Patch Changes + +- fd7a77b9c4: Surface additional context and details to the Backstage UI when the Vault plugin encounters non-successful HTTP responses from the Vault API. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-vault-backend@0.2.8 + +### Patch Changes + +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-xcmetrics@0.2.35 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/errors@1.1.4 + +## example-app@0.2.80 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0 + - @backstage/plugin-scaffolder@1.11.0 + - @backstage/plugin-azure-devops@0.2.6 + - @backstage/cli@0.22.2 + - @backstage/plugin-techdocs@1.5.0 + - @backstage/core-components@0.12.4 + - @backstage/plugin-microsoft-calendar@0.1.0 + - @backstage/plugin-catalog-graph@0.2.27 + - @backstage/plugin-code-coverage@0.2.8 + - @backstage/plugin-azure-sites@0.1.4 + - @backstage/plugin-lighthouse@0.4.0 + - @backstage/plugin-shortcuts@0.3.7 + - @backstage/plugin-home@0.4.31 + - @backstage/plugin-tech-insights@0.3.7 + - @backstage/plugin-search@1.1.0 + - @backstage/plugin-api-docs@0.9.0 + - @backstage/plugin-entity-feedback@0.1.0 + - @backstage/plugin-tech-radar@0.6.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10 + - @backstage/plugin-dynatrace@2.0.0 + - @backstage/plugin-playlist@0.1.6 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/plugin-kubernetes@0.7.8 + - @backstage/core-plugin-api@1.4.0 + - @backstage/app-defaults@1.2.0 + - @backstage/plugin-user-settings@0.7.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-pagerduty@0.5.8 + - @backstage/plugin-explore@0.4.0 + - @backstage/plugin-linguist@0.1.0 + - @backstage/plugin-linguist-common@0.1.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/plugin-apache-airflow@0.2.8 + - @backstage/plugin-circleci@0.3.15 + - @backstage/plugin-sentry@0.5.0 + - @backstage/plugin-org@0.6.5 + - @backstage/plugin-cost-insights@0.12.4 + - @backstage/plugin-airbrake@0.3.15 + - @internal/plugin-catalog-customized@0.0.7 + - @backstage/config@1.0.6 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-badges@0.2.39 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-catalog-import@0.9.5 + - @backstage/plugin-cloudbuild@0.3.15 + - @backstage/plugin-gcalendar@0.3.11 + - @backstage/plugin-gcp-projects@0.3.34 + - @backstage/plugin-github-actions@0.5.15 + - @backstage/plugin-gocd@0.1.21 + - @backstage/plugin-graphiql@0.2.47 + - @backstage/plugin-jenkins@0.7.14 + - @backstage/plugin-kafka@0.3.15 + - @backstage/plugin-newrelic@0.3.33 + - @backstage/plugin-newrelic-dashboard@0.2.8 + - @backstage/plugin-permission-react@0.4.10 + - @backstage/plugin-rollbar@0.4.15 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-stack-overflow@0.1.11 + - @backstage/plugin-techdocs-react@1.1.3 + - @backstage/plugin-todo@0.2.17 + +## example-backend@0.2.80 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/plugin-playlist-backend@0.2.5 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3 + - @backstage/backend-common@0.18.2 + - @backstage/plugin-kubernetes-backend@0.9.3 + - @backstage/plugin-lighthouse-backend@0.1.0 + - @backstage/plugin-code-coverage-backend@0.2.8 + - @backstage/plugin-azure-devops-backend@0.3.21 + - @backstage/plugin-azure-sites-backend@0.1.4 + - @backstage/plugin-adr-backend@0.3.0 + - @backstage/plugin-tech-insights-backend@0.5.8 + - @backstage/plugin-tech-insights-node@0.4.0 + - @backstage/plugin-entity-feedback-backend@0.1.0 + - @backstage/plugin-search-backend@1.2.3 + - @backstage/plugin-techdocs-backend@1.5.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26 + - @backstage/plugin-scaffolder-backend@1.11.0 + - @backstage/plugin-events-backend@0.2.3 + - @backstage/plugin-kafka-backend@0.2.35 + - @backstage/plugin-proxy-backend@0.2.36 + - @backstage/plugin-app-backend@0.3.42 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-auth-backend@0.18.0 + - @backstage/plugin-linguist-backend@0.1.0 + - @backstage/plugin-events-node@0.2.3 + - example-app@0.2.80 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-badges-backend@0.1.36 + - @backstage/plugin-explore-backend@0.0.4 + - @backstage/plugin-graphql-backend@0.1.32 + - @backstage/plugin-jenkins-backend@0.1.32 + - @backstage/plugin-permission-backend@0.5.17 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + - @backstage/plugin-rollbar-backend@0.1.39 + - @backstage/plugin-search-backend-module-pg@0.5.3 + - @backstage/plugin-search-backend-node@1.1.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-todo-backend@0.1.39 + +## example-backend-next@0.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/plugin-scaffolder-backend@1.11.0 + - @backstage/plugin-app-backend@0.3.42 + - @backstage/backend-defaults@0.1.7 + +## techdocs-cli-embedded-app@0.2.79 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/cli@0.22.2 + - @backstage/plugin-techdocs@1.5.0 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/app-defaults@1.2.0 + - @backstage/config@1.0.6 + - @backstage/integration-react@1.1.10 + - @backstage/test-utils@1.2.5 + - @backstage/plugin-techdocs-react@1.1.3 + +## @internal/plugin-catalog-customized@0.0.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/plugin-catalog-react@1.3.0 + +## @internal/plugin-todo-list@1.0.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## @internal/plugin-todo-list-backend@1.0.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 diff --git a/docs/releases/v1.11.0-next.0-changelog.md b/docs/releases/v1.11.0-next.0-changelog.md new file mode 100644 index 0000000000..2934e8054e --- /dev/null +++ b/docs/releases/v1.11.0-next.0-changelog.md @@ -0,0 +1,1429 @@ +# Release v1.11.0-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.2.0-next.0 + +### Minor Changes + +- 1ba120faa3: Added new mechanism to handle deltas in incremental providers + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-catalog-react@1.3.0-next.0 + +### Minor Changes + +- 929e1afe1b: Add `initialFilter` prop to EntityLifecyclePicker. This allows you to set an initial lifecycle for the catalog. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + +## @backstage/plugin-kubernetes-common@0.6.0-next.0 + +### Minor Changes + +- 1728c1ef01: **BREAKING**: Renamed misspelled `LimitRangeFetchReponse` to `LimitRangeFetchResponse`. + +### Patch Changes + +- 2518ef5b8a: New K8s catalog entity annotations added that will replace now deprecated k8s annotations in the catalog-model package. K8s annotation imports should now be made from plugin-kubernetes-common. +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-scaffolder@1.11.0-next.0 + +### Minor Changes + +- d6bb12a2e7: - **Deprecation** - Deprecated the following exports, please import them directly from `@backstage/plugin-scaffolder-react` instead + + createScaffolderLayout + ScaffolderLayouts + LayoutOptions + LayoutTemplate + +- 8c2966536b: Embed scaffolder workflow in other components + +### Patch Changes + +- cbab8ac107: lock versions of `@rjsf/*-beta` packages +- d2ddde2108: Add `ScaffolderLayouts` to `NextScaffolderPage` +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/integration-react@1.1.9 + +## @backstage/plugin-scaffolder-backend@1.11.0-next.0 + +### Minor Changes + +- 0b2952ee4b: Added the option to overwrite files in the `targetPath` of the `template:fetch` action + +### Patch Changes + +- ad3edc402d: **Deprecations**: The following are deprecated and should instead be imported from the new package `@backstage/plugin-scaffolder-node`: + + - `ActionContext` + - `createTemplateAction` + - `TaskSecrets` + - `TemplateAction` + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-scaffolder-node@0.1.0-next.0 + +### Minor Changes + +- d72866f0cc: New package that takes over some of the types and functionality from `@backstage/plugin-scaffolder-backend` that are shared with other modules + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-scaffolder-react@1.1.0-next.0 + +### Minor Changes + +- 8c2966536b: Embed scaffolder workflow in other components + +### Patch Changes + +- cbab8ac107: lock versions of `@rjsf/*-beta` packages +- d2ddde2108: Add `ScaffolderLayouts` to `NextScaffolderPage` +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + +## @backstage/backend-app-api@0.3.2-next.0 + +### Patch Changes + +- a18da2f8b5: Fixed an issue were the log redaction didn't properly escape RegExp characters. +- ed8b5967d7: `HttpRouterFactoryOptions.getPath` is now optional as a default value is always provided in the factory. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/backend-common@0.18.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.3.2-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/backend-defaults@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.3.2-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/backend-plugin-api@0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + +## @backstage/backend-tasks@0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/backend-test-utils@0.1.34-next.0 + +### Patch Changes + +- d0901c9ba4: All mock service factories and mock service implementations are now available via the new experimental `mockServices` export. +- Updated dependencies + - @backstage/backend-app-api@0.3.2-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/cli@0.22.1 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/catalog-client@1.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/catalog-model@1.1.6-next.0 + +### Patch Changes + +- 2518ef5b8a: Add deprecation tag to kubernetes constants in catalog, constants are now defined in plugin-kubernetes-common + +## @backstage/create-app@0.4.37-next.0 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/dev-utils@1.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/integration-react@1.1.9 + +## @techdocs/cli@1.3.2-next.0 + +### Patch Changes + +- 6e0b6a0d50: Fixed publish command missing awsBucketRootPath option. + Fixed publish command having the gcsBucketRootPath option misconfigured, previously returning a boolean vs a string. +- Updated dependencies + - @backstage/plugin-techdocs-node@1.4.6-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-adr@0.3.1-next.0 + +### Patch Changes + +- 496cf6aff0: Render the common `<MissingAnnotationEmptyState />` component when the `backstage.io/adr-location` annotation is missing from the component +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-adr-common@0.2.6-next.0 + - @backstage/integration-react@1.1.9 + +## @backstage/plugin-adr-backend@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-adr-common@0.2.6-next.0 + +## @backstage/plugin-adr-common@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-airbrake@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/dev-utils@1.0.12-next.0 + +## @backstage/plugin-airbrake-backend@0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-allure@0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-api-docs@0.8.15-next.0 + +### Patch Changes + +- 60996f8aba: Updating the readme with better `csp` instructions +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + +## @backstage/plugin-app-backend@0.3.42-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-auth-backend@0.17.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + +## @backstage/plugin-auth-node@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-azure-devops@0.2.6-next.0 + +### Patch Changes + +- 347b16ecea: Fixed `AssignedToCurrentUsersTeams` & `CreatedByCurrentUsersTeams` filter in `AzurePullRequestsPage` component. +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-azure-devops-backend@0.3.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-azure-sites@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-azure-sites-backend@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-badges@0.2.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-badges-backend@0.1.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + +## @backstage/plugin-bazaar@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/cli@0.22.1 + +## @backstage/plugin-bazaar-backend@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + +## @backstage/plugin-bitrise@0.1.42-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-catalog@1.7.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/integration-react@1.1.9 + +## @backstage/plugin-catalog-backend@1.7.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.1.15-next.0 + +### Patch Changes + +- 8e025f1347: Added support for `externalId` when assuming role in `AwsS3EntityProvider` +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.4.8-next.0 + +### Patch Changes + +- 0daa328c3a: Extract default transformers to their own file +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + +## @backstage/plugin-catalog-common@1.0.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-catalog-graph@0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + +## @backstage/plugin-catalog-graphql@0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-catalog-import@0.9.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/integration-react@1.1.9 + +## @backstage/plugin-catalog-node@1.3.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-cicd-statistics@0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-cicd-statistics@0.1.17-next.0 + +## @backstage/plugin-circleci@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-cloudbuild@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-code-climate@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-code-coverage@0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-code-coverage-backend@0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + +## @backstage/plugin-cost-insights@0.12.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-dynatrace@2.0.0-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-events-backend@0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-events-backend-module-azure@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-events-backend-module-github@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-events-backend-module-gitlab@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-events-backend-test-utils@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3-next.0 + +## @backstage/plugin-events-node@0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + +## @backstage/plugin-explore@0.3.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-explore-react@0.0.25 + +## @backstage/plugin-explore-backend@0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-firehydrant@0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + +## @backstage/plugin-fossa@0.2.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-github-actions@0.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-github-deployments@0.1.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/integration-react@1.1.9 + +## @backstage/plugin-github-issues@0.2.4-next.0 + +### Patch Changes + +- 3f88ae9d0a: Updated README.md examples to use correct components and fixed some syntax errors. +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-github-pull-requests-board@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-gocd@0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-graphql-backend@0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-graphql@0.3.18-next.0 + +## @backstage/plugin-home@0.4.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-stack-overflow@0.1.11-next.0 + +## @backstage/plugin-ilert@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-jenkins@0.7.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-jenkins-common@0.1.13-next.0 + +## @backstage/plugin-jenkins-backend@0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-jenkins-common@0.1.13-next.0 + +## @backstage/plugin-jenkins-common@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.11-next.0 + +## @backstage/plugin-kafka@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-kafka-backend@0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-kubernetes@0.7.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-kubernetes-backend@0.9.3-next.0 + +### Patch Changes + +- 2518ef5b8a: Adding new Cluster detail fields to catalogClusterLocator. Replace deprecated imports with k8s annotations from plugin-kubernetes-common. +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + +## @backstage/plugin-lighthouse@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-newrelic-dashboard@0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-org@0.6.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-org-react@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + +## @backstage/plugin-pagerduty@0.5.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-periskop@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-periskop-backend@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-permission-backend@0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + +## @backstage/plugin-permission-node@0.7.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + +## @backstage/plugin-playlist@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + +## @backstage/plugin-playlist-backend@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + +## @backstage/plugin-proxy-backend@0.2.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-rollbar@0.4.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-rollbar-backend@0.1.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.17-next.0 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + - @backstage/plugin-scaffolder-backend@1.11.0-next.0 + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.0 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + - @backstage/plugin-scaffolder-backend@1.11.0-next.0 + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.2-next.0 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.15-next.0 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + +## @backstage/plugin-scaffolder-common@1.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-search@1.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-search-backend@1.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + - @backstage/plugin-search-backend-node@1.1.3-next.0 + +## @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.1.3-next.0 + +## @backstage/plugin-search-backend-module-pg@0.5.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-search-backend-node@1.1.3-next.0 + +## @backstage/plugin-search-backend-node@1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + +## @backstage/plugin-sentry@0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-sonarqube@0.6.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-sonarqube-react@0.1.2-next.0 + +## @backstage/plugin-sonarqube-backend@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-sonarqube-react@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-splunk-on-call@0.4.4-next.0 + +### Patch Changes + +- 3598136ac7: Refactor plugin Card component to not rerender contents unnecessarily. +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-stack-overflow@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home@0.4.31-next.0 + +## @backstage/plugin-stack-overflow-backend@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-tech-insights@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-tech-insights-backend@0.5.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-tech-insights-node@0.3.10-next.0 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.0 + +### Patch Changes + +- d6b912f963: Surface the cause of the json rules engine +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-tech-insights-node@0.3.10-next.0 + +## @backstage/plugin-tech-insights-node@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + +## @backstage/plugin-techdocs@1.4.4-next.0 + +### Patch Changes + +- c8e09cc383: Fixed bug in Techdocs reader where a techdocs page with a hash in the URL did not always jump to the document anchor. +- cad5607411: Improve view: remove footer overlay on large screen +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/integration-react@1.1.9 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.4-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/integration-react@1.1.9 + +## @backstage/plugin-techdocs-backend@1.5.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.4.6-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/integration-react@1.1.9 + +## @backstage/plugin-techdocs-node@1.4.6-next.0 + +### Patch Changes + +- 87ab76e55c: Fixed bug caused by recent migration to AWS SDK V3 for techdocs-node. Instead of s3ForcePathStyle, forcePathStyle should be passed. +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + +## @backstage/plugin-techdocs-react@1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-todo@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-todo-backend@0.1.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + +## @backstage/plugin-user-settings@0.6.3-next.0 + +### Patch Changes + +- c4940b6322: Added a message to advise a page reload when toggling feature flags +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + +## @backstage/plugin-user-settings-backend@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + +## @backstage/plugin-vault@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-vault-backend@0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + +## example-app@0.2.80-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-azure-devops@0.2.6-next.0 + - @backstage/plugin-scaffolder-react@1.1.0-next.0 + - @backstage/plugin-scaffolder@1.11.0-next.0 + - @backstage/plugin-api-docs@0.8.15-next.0 + - @backstage/plugin-techdocs@1.4.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/plugin-user-settings@0.6.3-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-kubernetes@0.7.8-next.0 + - @backstage/plugin-airbrake@0.3.15-next.0 + - @backstage/plugin-azure-sites@0.1.4-next.0 + - @backstage/plugin-badges@0.2.39-next.0 + - @internal/plugin-catalog-customized@0.0.7-next.0 + - @backstage/plugin-catalog-graph@0.2.27-next.0 + - @backstage/plugin-catalog-import@0.9.5-next.0 + - @backstage/plugin-circleci@0.3.15-next.0 + - @backstage/plugin-cloudbuild@0.3.15-next.0 + - @backstage/plugin-code-coverage@0.2.8-next.0 + - @backstage/plugin-cost-insights@0.12.4-next.0 + - @backstage/plugin-dynatrace@2.0.0-next.0 + - @backstage/plugin-explore@0.3.46-next.0 + - @backstage/plugin-github-actions@0.5.15-next.0 + - @backstage/plugin-gocd@0.1.21-next.0 + - @backstage/plugin-home@0.4.31-next.0 + - @backstage/plugin-jenkins@0.7.14-next.0 + - @backstage/plugin-kafka@0.3.15-next.0 + - @backstage/plugin-lighthouse@0.3.15-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.8-next.0 + - @backstage/plugin-org@0.6.5-next.0 + - @backstage/plugin-pagerduty@0.5.8-next.0 + - @backstage/plugin-playlist@0.1.6-next.0 + - @backstage/plugin-rollbar@0.4.15-next.0 + - @backstage/plugin-search@1.0.8-next.0 + - @backstage/plugin-sentry@0.4.8-next.0 + - @backstage/plugin-tech-insights@0.3.7-next.0 + - @backstage/plugin-todo@0.2.17-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/cli@0.22.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.0 + - @backstage/integration-react@1.1.9 + - @backstage/plugin-apache-airflow@0.2.7 + - @backstage/plugin-gcalendar@0.3.10 + - @backstage/plugin-gcp-projects@0.3.33 + - @backstage/plugin-graphiql@0.2.46 + - @backstage/plugin-newrelic@0.3.32 + - @backstage/plugin-shortcuts@0.3.6 + - @backstage/plugin-stack-overflow@0.1.11-next.0 + - @backstage/plugin-tech-radar@0.6.0 + +## example-backend@0.2.80-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-backend@0.9.3-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.0 + - @backstage/plugin-scaffolder-backend@1.11.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - example-app@0.2.80-next.0 + - @backstage/plugin-techdocs-backend@1.5.3-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-app-backend@0.3.42-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-adr-backend@0.2.7-next.0 + - @backstage/plugin-auth-backend@0.17.5-next.0 + - @backstage/plugin-badges-backend@0.1.36-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/plugin-code-coverage-backend@0.2.8-next.0 + - @backstage/plugin-jenkins-backend@0.1.32-next.0 + - @backstage/plugin-kafka-backend@0.2.35-next.0 + - @backstage/plugin-playlist-backend@0.2.5-next.0 + - @backstage/plugin-tech-insights-backend@0.5.8-next.0 + - @backstage/plugin-todo-backend@0.1.39-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-events-backend@0.2.3-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + - @backstage/plugin-rollbar-backend@0.1.39-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.3-next.0 + - @backstage/plugin-azure-devops-backend@0.3.21-next.0 + - @backstage/plugin-azure-sites-backend@0.1.4-next.0 + - @backstage/plugin-explore-backend@0.0.4-next.0 + - @backstage/plugin-graphql-backend@0.1.32-next.0 + - @backstage/plugin-permission-backend@0.5.17-next.0 + - @backstage/plugin-proxy-backend@0.2.36-next.0 + - @backstage/plugin-search-backend@1.2.3-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.0 + - @backstage/plugin-search-backend-node@1.1.3-next.0 + - @backstage/plugin-tech-insights-node@0.3.10-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + +## example-backend-next@0.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.11.0-next.0 + - @backstage/backend-defaults@0.1.7-next.0 + - @backstage/plugin-app-backend@0.3.42-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + +## techdocs-cli-embedded-app@0.2.79-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.4-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/cli@0.22.1 + - @backstage/integration-react@1.1.9 + +## @internal/plugin-catalog-customized@0.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + +## @internal/plugin-todo-list-backend@1.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 diff --git a/docs/releases/v1.11.0-next.1-changelog.md b/docs/releases/v1.11.0-next.1-changelog.md new file mode 100644 index 0000000000..aa9c447149 --- /dev/null +++ b/docs/releases/v1.11.0-next.1-changelog.md @@ -0,0 +1,2164 @@ +# Release v1.11.0-next.1 + +## @backstage/plugin-catalog@1.8.0-next.1 + +### Minor Changes + +- 0c1fc3986c: Added Markdown support in the `AboutCard` description section +- 0eaa579f89: The `CatalogSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-react@1.3.0-next.1 + +### Minor Changes + +- fab93c2fe8: Aligned buttons on "Unregister entity" dialog to keep them on the same line + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + +## @backstage/plugin-explore@0.4.0-next.1 + +### Minor Changes + +- 0eaa579f89: The `ToolSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.25 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-scaffolder-backend@1.11.0-next.1 + +### Minor Changes + +- 127154930f: Renamed the export `scaffolderCatalogModule` to `catalogModuleTemplateKind` in order to follow the new recommended naming patterns of backend system items. This is technically a breaking change but in an alpha export, so take care to change your imports if you have already migrated to the new backend system. + +### Patch Changes + +- 66cf22fdc4: Updated dependency `esbuild` to `^0.17.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## @backstage/plugin-search@1.1.0-next.1 + +### Minor Changes + +- 0eaa579f89: Update `SearchModal` component to use `SearchResult` extensions. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-react@1.5.0-next.0 + +### Minor Changes + +- 0eaa579f89: - Create the search results extensions, for more details see the documentation [here](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions); + - Update the `SearchResult`, `SearchResultList` and `SearchResultGroup` components to use extensions and default their props to optionally accept a query, when the query is not passed, the component tries to get it from the search context. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-tech-insights-node@0.4.0-next.1 + +### Minor Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-techdocs@1.5.0-next.1 + +### Minor Changes + +- 20840b36b4: Update DocsTable and EntityListDocsTable to accept overrides for Material Table options. +- 0eaa579f89: The `TechDocsSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + +## @backstage/app-defaults@1.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.9 + +## @backstage/backend-app-api@0.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-node@0.7.5-next.1 + +## @backstage/backend-common@0.18.2-next.1 + +### Patch Changes + +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-app-api@0.3.2-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/backend-defaults@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-app-api@0.3.2-next.1 + +## @backstage/backend-plugin-api@0.3.2-next.1 + +### Patch Changes + +- ae88f61e00: The `register` methods passed to `createBackendPlugin` and `createBackendModule` + now have dedicated `BackendPluginRegistrationPoints` and + `BackendModuleRegistrationPoints` arguments, respectively. This lets us make it + clear on a type level that it's not possible to pass in extension points as + dependencies to plugins (should only ever be done for modules). This has no + practical effect on code that was already well behaved. +- Updated dependencies + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/backend-tasks@0.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/backend-test-utils@0.1.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.0 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-app-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## @backstage/cli@0.22.2-next.0 + +### Patch Changes + +- 561df21ea3: The `backstage-cli repo test` command now sets a default Jest `--workerIdleMemoryLimit` of 1GB. If needed to ensure that tests are not run in band, `--maxWorkers=2` is set as well. This is the recommended workaround for dealing with Jest workers leaking memory and eventually hitting the heap limit. +- 2815981057: Show module name causing error during build +- 66cf22fdc4: Updated dependency `esbuild` to `^0.17.0`. +- 6d3abfded1: Switch to inline source maps for test transpilation, simplifying editor setups. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + +## @backstage/core-app-api@1.4.1-next.0 + +### Patch Changes + +- dff4d8ddb1: Fixed an issue where an explicit port the frontend base URL could break the app. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + +## @backstage/core-components@0.12.4-next.0 + +### Patch Changes + +- 910015f5b7: The Button component has been deprecated in favor of the LinkButton component +- 20840b36b4: Adds new type, TableOptions, extending Material Table Options. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.3 + +## @backstage/create-app@0.4.37-next.1 + +### Patch Changes + +- 86a8dfd7b0: Added a check to ensure that Yarn v1 is used when creating new projects. +- 0eaa579f89: Update `SearchPage` template to use `SearchResult` extensions. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## @backstage/dev-utils@1.0.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/app-defaults@1.1.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + +## @backstage/integration-react@1.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## @techdocs/cli@1.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/plugin-techdocs-node@1.4.6-next.1 + +## @backstage/test-utils@1.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + +## @backstage/plugin-adr@0.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.6-next.0 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-adr-backend@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.6-next.0 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-airbrake@0.3.15-next.1 + +### Patch Changes + +- 41377156d0: Adds a boolean helper function to airbrake plugin for use on the EntityPage to show/hide airbrake tab depending on whether the entity's catalog-info.yml has an airbrake id set in the metadata +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/dev-utils@1.0.12-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-airbrake-backend@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + +## @backstage/plugin-allure@0.1.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-analytics-module-ga@0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apache-airflow@0.2.8-next.0 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + +## @backstage/plugin-api-docs@0.8.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apollo-explorer@0.1.8-next.0 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-app-backend@0.3.42-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + +## @backstage/plugin-auth-backend@0.17.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## @backstage/plugin-auth-node@0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-azure-devops@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-devops-backend@0.3.21-next.1 + +### Patch Changes + +- cc926a59bd: Fixed a bug where the azure devops host in URLs on the readme card was being URL encoded, breaking hosts with ports. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-azure-sites-backend@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-badges-backend@0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-bazaar@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.0 + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-bazaar-backend@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## @backstage/plugin-bitrise@0.1.42-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-catalog-backend@1.7.2-next.1 + +### Patch Changes + +- 2380506364: The process of adding or modifying fields in the software-catalog search index has been simplified. For more details, see [how to customize fields in the Software Catalog index](https://backstage.io/docs/features/search/how-to-guides#how-to-customize-fields-in-the-software-catalog-index). +- 9573651919: The previous migration that adds the `search.original_value` column may leave some of the entities not updated. Add a migration script to trigger a reprocessing of the entities. +- fc73f6aae5: Switched the order of reprocessing statements retroactively in migrations. This only improves the experience for those who at a later time perform a large upgrade of an old Backstage installation. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-backend-module-aws@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## @backstage/plugin-catalog-backend-module-azure@0.1.13-next.1 + +### Patch Changes + +- 2890f47517: This will add the ability to use Azure DevOps with multi project with a single value which is a new feature as previously this had to be done manually for each project that you wanted to add. + + Right now you would have to fill in multiple values in the config to use multiple projects: + + yourFirstProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: 'firstProject' # this will match the firstProject project + repository: '*' # this will match all repos + path: /catalog-info.yaml + yourSecondProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: 'secondProject' # this will match the secondProject project + repository: '*' # this will match all repos + path: /catalog-info.yaml + + With this change you can actually have all projects available where your PAT determines which you have access to, so that includes multiple projects: + + yourFirstProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: '*' # this will match all projects where your PAT has access to + repository: '*' # this will match all repos + path: /catalog-info.yaml + +- 85b04f659a: Internal refactor to not use deprecated `substr` + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.9-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## @backstage/plugin-catalog-backend-module-github@0.2.5-next.1 + +### Patch Changes + +- 66158754b4: Add support for filtering out forks +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.13-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 52c5685ceb: Implement Group and User Catalog Provider +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.2.0-next.1 + +### Patch Changes + +- b7e36660d5: Return `EventSubscriber` from `addIncrementalEntityProvider` to hook up to `EventsBackend` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.4.8-next.1 + +### Patch Changes + +- 4c86436fdf: Fix MS Graph provider to use target URL for fetching access token +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## @backstage/plugin-catalog-graph@0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-catalog-import@0.9.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + +## @backstage/plugin-catalog-node@1.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + +## @backstage/plugin-cicd-statistics@0.1.17-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cicd-statistics@0.1.17-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + +## @backstage/plugin-circleci@0.3.15-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-cloudbuild@0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-climate@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-coverage@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-coverage-backend@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-codescene@0.1.10-next.0 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-config-schema@0.1.38-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## @backstage/plugin-cost-insights@0.12.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-dynatrace@2.0.0-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-events-backend@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-events-backend-module-aws-sqs@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-events-backend-module-azure@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-events-backend-module-gerrit@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-events-backend-module-github@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-events-backend-module-gitlab@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-events-backend-test-utils@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3-next.1 + +## @backstage/plugin-events-node@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + +## @backstage/plugin-explore-backend@0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-firehydrant@0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-fossa@0.2.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcalendar@0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcp-projects@0.3.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-git-release-manager@0.3.28-next.0 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-actions@0.5.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-deployments@0.1.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-issues@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-pull-requests-board@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gitops-profiles@0.3.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gocd@0.1.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphiql@0.2.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphql-backend@0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.18-next.0 + +## @backstage/plugin-home@0.4.31-next.1 + +### Patch Changes + +- 3d1d867d42: Fixed regression that caused the `WelcomeTitle` to not be the right size when passed to the `title` property of the `<Header>` component. A Storybook entry was also added for the `WelcomeTitle` +- c553a625d2: remove unused plugin-stack-overflow dependency +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-ilert@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-jenkins@0.7.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-jenkins-common@0.1.13-next.0 + +## @backstage/plugin-jenkins-backend@0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-jenkins-common@0.1.13-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-kafka@0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-kafka-backend@0.2.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-kubernetes@0.7.8-next.1 + +### Patch Changes + +- 145a79a15b: Condenses kubernetes ui plugin to fit more onscreen and increase visibility +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.6.0-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-kubernetes-backend@0.9.3-next.1 + +### Patch Changes + +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/plugin-kubernetes-common@0.6.0-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## @backstage/plugin-kubernetes-common@0.6.0-next.1 + +### Patch Changes + +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## @backstage/plugin-lighthouse@0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic@0.3.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic-dashboard@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-org@0.6.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-org-react@0.1.4-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-pagerduty@0.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-periskop@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-periskop-backend@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + +## @backstage/plugin-permission-backend@0.5.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + +## @backstage/plugin-permission-node@0.7.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-playlist@0.1.6-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist-common@0.1.4 + +## @backstage/plugin-playlist-backend@0.2.5-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + - @backstage/plugin-playlist-common@0.1.4 + +## @backstage/plugin-proxy-backend@0.2.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + +## @backstage/plugin-rollbar@0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-rollbar-backend@0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + +## @backstage/plugin-scaffolder@1.11.0-next.1 + +### Patch Changes + +- 04f717a8e1: `scaffolder/next`: bump `react-jsonschema-form` libraries to `v5-stable` +- 346d6b6630: Upgrade `@rjsf` version 5 dependencies to `beta.18` +- 0f0da2f256: Prefer schema ordering of template properties during review content generation. +- 38992bdbaf: Fixed bug in review step refactor that caused schema-based display settings for individual property values to be discarded. +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/plugin-scaffolder-backend@1.11.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/plugin-scaffolder-backend@1.11.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.2-next.1 + +### Patch Changes + +- da418c89e4: Fix broken module exports and dependencies to match a backend module, rather than a frontend plugin. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## @backstage/plugin-scaffolder-node@0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + +## @backstage/plugin-scaffolder-react@1.1.0-next.1 + +### Patch Changes + +- 04f717a8e1: `scaffolder/next`: bump `react-jsonschema-form` libraries to `v5-stable` +- 346d6b6630: Upgrade `@rjsf` version 5 dependencies to `beta.18` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + +## @backstage/plugin-search-backend@1.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + - @backstage/plugin-search-backend-node@1.1.3-next.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-pg@0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-node@1.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-sentry@0.4.8-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-shortcuts@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## @backstage/plugin-sonarqube@0.6.3-next.1 + +### Patch Changes + +- 6310eacc11: Additional export added in order to bind SonarQubeClient to its apiref +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-sonarqube-react@0.1.2-next.0 + +## @backstage/plugin-sonarqube-backend@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-splunk-on-call@0.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-stack-overflow@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home@0.4.31-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-stack-overflow-backend@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-tech-insights@0.3.7-next.1 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## @backstage/plugin-tech-insights-backend@0.5.8-next.1 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-tech-insights-common@0.2.10-next.0 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/types@1.0.2 + +## @backstage/plugin-tech-radar@0.6.1-next.0 + +### Patch Changes + +- 18024a231c: Allow to set additional links to the entry description. +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.10-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/plugin-techdocs@1.5.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + +## @backstage/plugin-techdocs-backend@1.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-node@1.4.6-next.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + +## @backstage/plugin-techdocs-node@1.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-techdocs-react@1.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/version-bridge@1.0.3 + +## @backstage/plugin-todo@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-todo-backend@0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-user-settings@0.6.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## @backstage/plugin-user-settings-backend@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## @backstage/plugin-vault@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-vault-backend@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-xcmetrics@0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## example-app@0.2.80-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.1 + - @backstage/plugin-scaffolder@1.11.0-next.1 + - @backstage/cli@0.22.2-next.0 + - @backstage/plugin-techdocs@1.5.0-next.1 + - @backstage/plugin-tech-insights@0.3.7-next.1 + - @backstage/plugin-search@1.1.0-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.1 + - @backstage/plugin-dynatrace@2.0.0-next.1 + - @backstage/plugin-playlist@0.1.6-next.1 + - @backstage/plugin-home@0.4.31-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/plugin-kubernetes@0.7.8-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-tech-radar@0.6.1-next.0 + - @backstage/plugin-explore@0.4.0-next.1 + - @backstage/plugin-apache-airflow@0.2.8-next.0 + - @backstage/plugin-circleci@0.3.15-next.1 + - @backstage/plugin-sentry@0.4.8-next.1 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-airbrake@0.3.15-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/app-defaults@1.1.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-api-docs@0.8.15-next.1 + - @backstage/plugin-azure-devops@0.2.6-next.1 + - @backstage/plugin-azure-sites@0.1.4-next.1 + - @backstage/plugin-badges@0.2.39-next.1 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-graph@0.2.27-next.1 + - @backstage/plugin-catalog-import@0.9.5-next.1 + - @backstage/plugin-cloudbuild@0.3.15-next.1 + - @backstage/plugin-code-coverage@0.2.8-next.1 + - @backstage/plugin-cost-insights@0.12.4-next.1 + - @backstage/plugin-gcalendar@0.3.11-next.0 + - @backstage/plugin-gcp-projects@0.3.34-next.0 + - @backstage/plugin-github-actions@0.5.15-next.1 + - @backstage/plugin-gocd@0.1.21-next.1 + - @backstage/plugin-graphiql@0.2.47-next.0 + - @backstage/plugin-jenkins@0.7.14-next.1 + - @backstage/plugin-kafka@0.3.15-next.1 + - @backstage/plugin-lighthouse@0.3.15-next.1 + - @backstage/plugin-newrelic@0.3.33-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.8-next.1 + - @backstage/plugin-org@0.6.5-next.1 + - @backstage/plugin-pagerduty@0.5.8-next.1 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-rollbar@0.4.15-next.1 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-shortcuts@0.3.7-next.0 + - @backstage/plugin-stack-overflow@0.1.11-next.1 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + - @backstage/plugin-todo@0.2.17-next.1 + - @backstage/plugin-user-settings@0.6.3-next.1 + - @internal/plugin-catalog-customized@0.0.7-next.1 + +## example-backend@0.2.80-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/plugin-tech-insights-backend@0.5.8-next.1 + - @backstage/plugin-tech-insights-node@0.4.0-next.1 + - @backstage/plugin-azure-devops-backend@0.3.21-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/plugin-kubernetes-backend@0.9.3-next.1 + - @backstage/plugin-scaffolder-backend@1.11.0-next.1 + - @backstage/plugin-playlist-backend@0.2.5-next.1 + - example-app@0.2.80-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-backend@0.2.7-next.1 + - @backstage/plugin-app-backend@0.3.42-next.1 + - @backstage/plugin-auth-backend@0.17.5-next.1 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-azure-sites-backend@0.1.4-next.1 + - @backstage/plugin-badges-backend@0.1.36-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-code-coverage-backend@0.2.8-next.1 + - @backstage/plugin-events-backend@0.2.3-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + - @backstage/plugin-explore-backend@0.0.4-next.1 + - @backstage/plugin-graphql-backend@0.1.32-next.1 + - @backstage/plugin-jenkins-backend@0.1.32-next.1 + - @backstage/plugin-kafka-backend@0.2.35-next.1 + - @backstage/plugin-permission-backend@0.5.17-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + - @backstage/plugin-proxy-backend@0.2.36-next.1 + - @backstage/plugin-rollbar-backend@0.1.39-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.1 + - @backstage/plugin-search-backend@1.2.3-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.1 + - @backstage/plugin-search-backend-module-pg@0.5.3-next.1 + - @backstage/plugin-search-backend-node@1.1.3-next.1 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.1 + - @backstage/plugin-techdocs-backend@1.5.3-next.1 + - @backstage/plugin-todo-backend@0.1.39-next.1 + +## example-backend-next@0.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/plugin-scaffolder-backend@1.11.0-next.1 + - @backstage/backend-defaults@0.1.7-next.1 + - @backstage/plugin-app-backend@0.3.42-next.1 + +## techdocs-cli-embedded-app@0.2.79-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.0 + - @backstage/plugin-techdocs@1.5.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/app-defaults@1.1.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + +## @internal/plugin-catalog-customized@0.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.1 + +## @internal/plugin-todo-list@1.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @internal/plugin-todo-list-backend@1.0.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 diff --git a/docs/releases/v1.11.0-next.2-changelog.md b/docs/releases/v1.11.0-next.2-changelog.md new file mode 100644 index 0000000000..65f6622921 --- /dev/null +++ b/docs/releases/v1.11.0-next.2-changelog.md @@ -0,0 +1,2289 @@ +# Release v1.11.0-next.2 + +## @backstage/backend-app-api@0.4.0-next.2 + +### Minor Changes + +- 01a075ec1d: **BREAKING**: Renamed `RootHttpRouterConfigureOptions` to `RootHttpRouterConfigureContext`, and removed the unused type `ServiceOrExtensionPoint`. +- 4ae71b7f2e: **BREAKING** Renaming `*Factory` exports to `*ServiceFactory` instead. For example `configFactory` now is exported as `configServiceFactory`. +- d31d8e00b3: **BREAKING** `HttpServerCertificateOptions` when specified with a `key` and `cert` should also have the `type: 'pem'` instead of `type: 'plain'` + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- f60cca9da1: Updated database factory to pass service deps required for restoring database state during development. +- 610d65e143: Updates to match new `BackendFeature` type. +- ab22515647: The shutdown signal handlers are now installed as part of the backend instance rather than the lifecycle service, and explicitly cause the process to exit. +- b729f9f31f: Moved the options of the `config` and `rootHttpRouter` services out to the factories themselves, where they belong +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/backend-dev-utils@0.1.0-next.0 + +### Minor Changes + +- 9d9cdea117: Introduced a new package for backend development utilities. Similar to how `@backstage/dev-utils` is used in the frontend. + +## @backstage/backend-plugin-api@0.4.0-next.2 + +### Minor Changes + +- e716946103: **BREAKING**: Split out the hook for both lifecycle services so that the first parameter of `addShutdownHook` is the hook function, and the second is the options. +- 0ff03319be: **BREAKING**: The plugin ID option passed to `createBackendPlugin` is now `pluginId`, rather than just `id`. This is to make it match `createBackendModule` more closely. +- 71a5ec0f06: **BREAKING**: Switched out `LogMeta` type for `JsonObject`. +- 610d65e143: Switched `BackendFeature` to be an opaque type. + +### Patch Changes + +- 9c9456fd33: Removed the unused `TypesToServiceRef` type +- 181c03edb5: Aligned opaque type markers to all use a `$type` property with namespacing. +- Updated dependencies + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/catalog-model@1.2.0-next.1 + +### Minor Changes + +- d51668882d: Adds the tRPC API type to the catalog. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/eslint-plugin@0.1.0-next.0 + +### Minor Changes + +- 179d301518: Added a new ESLint plugin with common rules for Backstage projects. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for more details. + +## @backstage/plugin-adr@0.4.0-next.2 + +### Minor Changes + +- ec34b535c0: The `AdrSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- ec34b535c0: Support displaying an icon on `AdrSearchResultListItem` +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.6-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-api-docs@0.9.0-next.2 + +### Minor Changes + +- 9ed53218a5: Adds a new tRPC API definition widget which users can utilize to integrate their tRPC API definitions into Backstage. + +### Patch Changes + +- 6856ab98f4: Updated dependency `@asyncapi/react-component` to `1.0.0-next.47`. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.0-next.2 + +### Minor Changes + +- fb568e2683: Improve performance when loading users via group membership. + Users data is now loaded from a paged query, rather than having to make an extra call per user to load each user's profiles. + + Note, there are still additional per user calls made to load user avatars + +### Patch Changes + +- 28f9883440: Fixed a bug reading the `user.select` field expected from the `app-config.yaml` configuration +- c5b119ad9c: Increased default page size to 999 (from 100) to reduce the number of calls made to the Microsoft Graph API. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-entity-validation@0.1.0-next.0 + +### Minor Changes + +- d34b3267f9: First implementation for the entity validation UI. For more information refer to its `README.md`. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-graphql-voyager@0.1.0-next.0 + +### Minor Changes + +- d1fb4b7bf1: Add the new GraphQL Voyager tool as a plugin for backstage, expose necessary API and components needed for it. + + For more information on how to use this plugin and configure it, please navigate to the [README](https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager/README.md) of the plugin. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-lighthouse@0.4.0-next.2 + +### Minor Changes + +- eef62546ce: Require @backstage/plugin-lighthouse-common package where API implementation moved to. + + **BREAKING**: the following types have been moved to @backstage/plugin-lighthouse-common: + + - `Audit` + - `AuditBase` + - `AuditCompleted` + - `AuditFailed` + - `AuditRunning` + - `FetchError` + - `LASListRequest` + - `LASListResponse` + - `LighthouseApi` + - `LighthouseCategoryAbbr` + - `LighthouseCategoryId` + - `LighthouseConfigSettings` + - `LighthouseRestApi` + - `TriggerAuditPayload` + - `Website` + - `WebsiteListResponse` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-common@0.1.0-next.0 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-lighthouse-backend@0.1.0-next.0 + +### Minor Changes + +- eef62546ce: Introduce Lighthouse Backend Plugin to run scheduled Lighthouse Audits + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-common@0.1.0-next.0 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-lighthouse-common@0.1.0-next.0 + +### Minor Changes + +- eef62546ce: Introduce @backstage/plugin-lighthouse-common with the API implementation + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + +## @backstage/plugin-scaffolder-react@1.1.0-next.2 + +### Minor Changes + +- 5555e17313: refactor `createAsyncValidators` to be recursive to ensure validators are called in nested schemas. + +### Patch Changes + +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## @backstage/plugin-sentry@0.5.0-next.2 + +### Minor Changes + +- 6a8d6b9a0c: Add option to filter issues based on the time it was triggered + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/app-defaults@1.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.9 + +## @backstage/backend-common@0.18.2-next.2 + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- d31d8e00b3: Updated to work with the new `type: 'pem'` with `createHttpServer` from `@backstage/backend-app-api` +- 0ff03319be: Updated usage of `createBackendPlugin`. +- f60cca9da1: The `DatabaseManager.forPlugin` method now accepts additional service dependencies. There is no need to update existing code to pass these dependencies. +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-dev-utils@0.1.0-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/backend-defaults@0.1.7-next.2 + +### Patch Changes + +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + +## @backstage/backend-tasks@0.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/backend-test-utils@0.1.34-next.2 + +### Patch Changes + +- baf6e4c96a: Removed unnecessary `@backstage/cli` dependency. +- 1835311713: Added explicit return type signature for `mockServices.config()`. +- e716946103: Updated usage of the lifecycle service. +- 7e7557a2be: Updated the `mockServices.rootLogger` options to accept a single level option instead. +- 610d65e143: Updates to match new `BackendFeature` type. +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- f1adb2e36b: Removed the `ref` from all `mockServices`. +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/catalog-client@1.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/errors@1.1.4 + +## @backstage/cli@0.22.2-next.1 + +### Patch Changes + +- dd8a9afe66: Replaced several monorepo lint rules with new rules from `@backstage/eslint-plugin`. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for a full list of rules. +- 90616df9a8: Added an experimental mode for the `package start` command for backend packages. Enabled by setting `EXPERIMENTAL_BACKEND_START`. +- Updated dependencies + - @backstage/eslint-plugin@0.1.0-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + +## @backstage/codemods@0.1.43-next.0 + +### Patch Changes + +- 02f1316e57: Moved `commander` to being a regular dependency. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## @backstage/core-components@0.12.4-next.1 + +### Patch Changes + +- 66e2aab4c4: Navigation items in mobile sidebar now have aria label. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.3 + +## @backstage/create-app@0.4.37-next.2 + +### Patch Changes + +- 02f1316e57: Updated `packages/app/cypress/.eslintrc.json` to remove the unnecessary `import/no-extraneous-dependencies` rule. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## @backstage/dev-utils@1.0.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/integration-react@1.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## @backstage/repo-tools@0.1.2-next.0 + +### Patch Changes + +- ff63acf30a: Packages without a declared `backstage.role` are now ignored. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + +## @techdocs/cli@1.3.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/plugin-techdocs-node@1.4.6-next.2 + +## @backstage/plugin-adr-backend@0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.6-next.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-adr-common@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/integration@1.4.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-airbrake@0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/dev-utils@1.0.12-next.2 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-airbrake-backend@0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-allure@0.1.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-analytics-module-ga@0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apache-airflow@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + +## @backstage/plugin-apollo-explorer@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-app-backend@0.3.42-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + +## @backstage/plugin-auth-backend@0.17.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-auth-node@0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-azure-devops@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-azure-devops-backend@0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.4-next.2 + +### Patch Changes + +- 98a00a09d5: Fixed `AzureSites` plugin start and stop issue #15904 + Fixed `AzureSites` UI load issue #15907 +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-azure-sites-backend@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-badges-backend@0.1.36-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-bazaar@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-bazaar-backend@0.2.5-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-bitrise@0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-catalog@1.8.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-backend@1.7.2-next.2 + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-backend-module-aws@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-azure@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + - @backstage/plugin-catalog-common@1.0.11-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-catalog-backend-module-github@0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.13-next.2 + +### Patch Changes + +- 49948f644f: The config now consistently uses the term 'instance' to refer to a single GitLab API host. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.2.0-next.2 + +### Patch Changes + +- 407dc01fc9: Removing extra imports for `run` script as `TestBackend` auto loads the default factories +- 77c41b6924: Updated README to include newer API options for incremental entity providers +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-common@1.0.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-graph@0.2.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-catalog-graphql@0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-import@0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-catalog-node@1.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + +## @backstage/plugin-catalog-react@1.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + +## @backstage/plugin-cicd-statistics@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/plugin-cicd-statistics@0.1.17-next.2 + +## @backstage/plugin-circleci@0.3.15-next.2 + +### Patch Changes + +- 1616e06047: Making workflow a link +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-cloudbuild@0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-code-climate@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-code-coverage@0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-code-coverage-backend@0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-codescene@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-config-schema@0.1.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## @backstage/plugin-cost-insights@0.12.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-dynatrace@2.0.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-events-backend@0.2.3-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-module-aws-sqs@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-events-backend-module-azure@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + +## @backstage/plugin-events-backend-module-gerrit@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + +## @backstage/plugin-events-backend-module-github@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-module-gitlab@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-test-utils@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3-next.2 + +## @backstage/plugin-events-node@0.2.3-next.2 + +### Patch Changes + +- 19d4abf72c: Make `EventParams` typed for implementing tidier event handling. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + +## @backstage/plugin-explore@0.4.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.25 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-explore-backend@0.0.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-firehydrant@0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-fossa@0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-gcalendar@0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcp-projects@0.3.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-git-release-manager@0.3.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-actions@0.5.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-github-deployments@0.1.46-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-github-issues@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-github-pull-requests-board@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-gitops-profiles@0.3.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gocd@0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-graphiql@0.2.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphql-backend@0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.18-next.1 + +## @backstage/plugin-home@0.4.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-ilert@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-jenkins@0.7.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-jenkins-common@0.1.13-next.1 + +## @backstage/plugin-jenkins-backend@0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-jenkins-common@0.1.13-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-jenkins-common@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-kafka@0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-kafka-backend@0.2.35-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-kubernetes@0.7.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-kubernetes-common@0.6.0-next.2 + +## @backstage/plugin-kubernetes-backend@0.9.3-next.2 + +### Patch Changes + +- 7ff81f7692: Kubernetes proxy endpoint now accepts content types that are not json +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-kubernetes-common@0.6.0-next.2 + +## @backstage/plugin-kubernetes-common@0.6.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + +## @backstage/plugin-newrelic@0.3.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic-dashboard@0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-org@0.6.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-org-react@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-pagerduty@0.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-periskop@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-periskop-backend@0.1.13-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-permission-backend@0.5.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-permission-node@0.7.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-playlist@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist-common@0.1.4 + +## @backstage/plugin-playlist-backend@0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-playlist-common@0.1.4 + +## @backstage/plugin-proxy-backend@0.2.36-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-rollbar@0.4.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-rollbar-backend@0.1.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-scaffolder@1.11.0-next.2 + +### Patch Changes + +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## @backstage/plugin-scaffolder-backend@1.11.0-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-common@1.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-node@0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## @backstage/plugin-search@1.1.0-next.2 + +### Patch Changes + +- 66e2aab4c4: Updated colors for each tab used on search modal, to improve color contrast. Aria label added to tabs wrapper component. `disableRipple` property on the `Tab` component removed to improve keyboard navigation indicator. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend@1.2.3-next.2 + +### Patch Changes + +- 1b30c9a0f7: Change the router's response to include the error message instead of its object type in case it fails during a search query because the messages have more info. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-pg@0.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-node@1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-react@1.5.0-next.1 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-shortcuts@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## @backstage/plugin-sonarqube@0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-sonarqube-react@0.1.2-next.1 + +## @backstage/plugin-sonarqube-backend@0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-sonarqube-react@0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + +## @backstage/plugin-splunk-on-call@0.4.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-stack-overflow@0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-home@0.4.31-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-stack-overflow-backend@0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-tech-insights@0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + +## @backstage/plugin-tech-insights-backend@0.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + +## @backstage/plugin-tech-insights-node@0.4.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + +## @backstage/plugin-tech-radar@0.6.1-next.1 + +### Patch Changes + +- acf6c8c10b: Deprecate `RadarEntry.url` - use `RadarEntry.links` instead + + ```diff + - url: 'https://www.javascript.com/', + key: 'javascript', + id: 'javascript', + title: 'JavaScript', + quadrant: 'languages', + links: [ + + { + + url: 'https://www.javascript.com/', + + title: 'Learn more', + + }, + ], + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs@1.5.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## @backstage/plugin-techdocs-backend@1.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-node@1.4.6-next.2 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## @backstage/plugin-techdocs-node@1.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-techdocs-react@1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/version-bridge@1.0.3 + +## @backstage/plugin-todo@0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-todo-backend@0.1.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-user-settings@0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-user-settings-backend@0.1.6-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-vault@0.1.9-next.2 + +### Patch Changes + +- fd7a77b9c4: Surface additional context and details to the Backstage UI when the Vault plugin encounters non-successful HTTP responses from the Vault API. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-vault-backend@0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-xcmetrics@0.2.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## example-app@0.2.80-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.2 + - @backstage/plugin-scaffolder@1.11.0-next.2 + - @backstage/cli@0.22.2-next.1 + - @backstage/plugin-api-docs@0.9.0-next.2 + - @backstage/plugin-tech-radar@0.6.1-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-explore@0.4.0-next.2 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/plugin-sentry@0.5.0-next.2 + - @backstage/plugin-circleci@0.3.15-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/plugin-azure-sites@0.1.4-next.2 + - @backstage/plugin-search@1.1.0-next.2 + - @backstage/plugin-lighthouse@0.4.0-next.2 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-airbrake@0.3.15-next.2 + - @backstage/plugin-apache-airflow@0.2.8-next.1 + - @backstage/plugin-azure-devops@0.2.6-next.2 + - @backstage/plugin-badges@0.2.39-next.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-graph@0.2.27-next.2 + - @backstage/plugin-catalog-import@0.9.5-next.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-cloudbuild@0.3.15-next.2 + - @backstage/plugin-code-coverage@0.2.8-next.2 + - @backstage/plugin-cost-insights@0.12.4-next.2 + - @backstage/plugin-dynatrace@2.0.0-next.2 + - @backstage/plugin-gcalendar@0.3.11-next.1 + - @backstage/plugin-gcp-projects@0.3.34-next.1 + - @backstage/plugin-github-actions@0.5.15-next.2 + - @backstage/plugin-gocd@0.1.21-next.2 + - @backstage/plugin-graphiql@0.2.47-next.1 + - @backstage/plugin-home@0.4.31-next.2 + - @backstage/plugin-jenkins@0.7.14-next.2 + - @backstage/plugin-kafka@0.3.15-next.2 + - @backstage/plugin-kubernetes@0.7.8-next.2 + - @backstage/plugin-newrelic@0.3.33-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.8-next.2 + - @backstage/plugin-org@0.6.5-next.2 + - @backstage/plugin-pagerduty@0.5.8-next.2 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist@0.1.6-next.2 + - @backstage/plugin-rollbar@0.4.15-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-shortcuts@0.3.7-next.1 + - @backstage/plugin-stack-overflow@0.1.11-next.2 + - @backstage/plugin-tech-insights@0.3.7-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.2 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + - @backstage/plugin-todo@0.2.17-next.2 + - @backstage/plugin-user-settings@0.6.3-next.2 + - @internal/plugin-catalog-customized@0.0.7-next.2 + +## example-backend@0.2.80-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-backend@0.1.0-next.0 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/plugin-search-backend@1.2.3-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-events-backend@0.2.3-next.2 + - @backstage/plugin-kafka-backend@0.2.35-next.2 + - @backstage/plugin-proxy-backend@0.2.36-next.2 + - @backstage/plugin-app-backend@0.3.42-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-kubernetes-backend@0.9.3-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-backend@0.17.5-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/plugin-playlist-backend@0.2.5-next.2 + - @backstage/plugin-rollbar-backend@0.1.39-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.3-next.2 + - @backstage/plugin-tech-insights-backend@0.5.8-next.2 + - @backstage/plugin-techdocs-backend@1.5.3-next.2 + - example-app@0.2.80-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-backend@0.2.7-next.2 + - @backstage/plugin-azure-devops-backend@0.3.21-next.2 + - @backstage/plugin-azure-sites-backend@0.1.4-next.2 + - @backstage/plugin-badges-backend@0.1.36-next.2 + - @backstage/plugin-code-coverage-backend@0.2.8-next.2 + - @backstage/plugin-explore-backend@0.0.4-next.2 + - @backstage/plugin-graphql-backend@0.1.32-next.2 + - @backstage/plugin-jenkins-backend@0.1.32-next.2 + - @backstage/plugin-permission-backend@0.5.17-next.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.2 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.2 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + - @backstage/plugin-todo-backend@0.1.39-next.2 + +## example-backend-next@0.0.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-app-backend@0.3.42-next.2 + - @backstage/backend-defaults@0.1.7-next.2 + +## techdocs-cli-embedded-app@0.2.79-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## @internal/plugin-catalog-customized@0.0.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @internal/plugin-todo-list@1.0.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @internal/plugin-todo-list-backend@1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 diff --git a/docs/releases/v1.11.0.md b/docs/releases/v1.11.0.md new file mode 100644 index 0000000000..1afed5fbb6 --- /dev/null +++ b/docs/releases/v1.11.0.md @@ -0,0 +1,100 @@ +--- +id: v1.11.0 +title: v1.11.0 +description: Backstage Release v1.11.0 +--- + +These are the release notes for the v1.11.0 release of [Backstage](https://backstage.io/). + +A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done. + +## Highlights + +### Scaffolder Alpha + +With this release, we’ve been working on getting things ready for testing for the upcoming Scaffolder v2.0.0 release, and there’s now the ability to test the upcoming version in the form of an alpha! It would be great if you have the time to test out the new release to see if all your templates work as expected. There’s some documentation on how to test out the new version and what’s new under [testing scaffolder alpha](https://backstage.io/docs/features/software-templates/testing-scaffolder-alpha). + +We’d love to hear if you have any issues when testing the new version, please reach out on Discord or [create a new issue](http://github.com/backstage/backstage/issues/new) if there’s anything you find! + +### Backend System Alpha + +The [new Backend System](https://backstage.io/docs/backend-system/) is now in alpha. We encourage plugin owners to try out the new system by adding support for it in their plugin backends, but we do not yet recommend migrating over to using this new system for production backend deployments. + +This new system aims to greatly simplify the setup and wiring of Backstage backends. It formalizes and simplifies how common services and plugins are installed and interact with each other, and restructures both plugins and backends to make them much easier to maintain. + +The documentation starts [here](https://backstage.io/docs/backend-system/). Migration guides exist to help you get your [backend](https://backstage.io/docs/backend-system/building-backends/migrating) and [plugins](https://backstage.io/docs/backend-system/building-plugins-and-modules/migrating) migrated, but you don’t have to do it all in one go - the path forward can be incremental. + +If you were already using the new backend system before this release, you may also be interested in the following. + +There have been a number of tweaks to types and exports, following a thorough API review before its alpha release. Notably, + +Service factories are now exported with the pattern `<id>ServiceFactory` instead of `<id>Factory`, e.g. `configServiceFactory` +The `createBackendPlugin` parameter that used to be named `id` is now `pluginId` +The interface of the `CacheService` has changed slightly, so you may want to use the new `cacheToPluginCacheManager` to get compatibility with your not-yet-converted backend plugins +The `scaffolderCatalogModule` that was exported from the Scaffolder backend is renamed to `catalogModuleTemplateKind`, to fit the proper naming patterns + +There are a few smaller tweaks as well, that you can read about in the [`@backstage/backend-app-api`](https://github.com/backstage/backstage/blob/master/packages/backend-app-api/CHANGELOG.md) and [`@backstage/backend-plugin-api`](https://github.com/backstage/backstage/blob/master/packages/backend-plugin-api/CHANGELOG.md) changelogs. + +### New package: `@backstage/eslint-plugin` + +We created a separate ESlint plugin package that houses some rules that are helpful for Backstage monorepos. Not only does it make a convenient bundle of them all, it also comes with a pretty healthy speed boost. [The pull request](https://github.com/backstage/backstage/pull/16174) contains some more details. + +### New plugin: Entity Feedback + +A plugin that allows users to rate the quality of entities in your Backstage catalog. Allowing your users to rate entities brings them into the feedback loop that can help notify owners if something is wrong with their entity (i.e. taking inspiration from the feedback model of TechDocs). Check out [the README](https://github.com/backstage/backstage/tree/master/plugins/entity-feedback)! + +Contributed by [@kuangp](https://github.com/kuangp) in [#15881](https://github.com/backstage/backstage/pull/15881) + +### New plugin: Entity Validator + +This plugin helps users create and edit their YAML definitions of entities, with live validation. It leverages the actual catalog backend for checks, so that you can submit catalog info file changes that fit your particular organization with greater confidence. Installation instructions and details are in [the README](https://github.com/backstage/backstage/tree/master/plugins/entity-validation). + +Contributed by [@ivangonzalezacuna](https://github.com/ivangonzalezacuna) in [#16050](https://github.com/backstage/backstage/pull/16050) + +### New Plugin: Microsoft Calendar + +This plugin lets you show your MS calendars directly in Backstage. More information and installation instructions in [the README](https://github.com/backstage/backstage/tree/master/plugins/microsoft-calendar)! + +Contributed by [@Abhay-soni-developer](https://github.com/Abhay-soni-developer) in [#16101](https://github.com/backstage/backstage/pull/16101) + +### New plugin: GraphQL Voyager + +This nifty plugin presents a graph structure of your entire GraphQL API. Check out [its README](https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager) for a screenshot and instructions on installing it. + +Contributed by [@MitchWijt](https://github.com/MitchWijt) in [#16015](https://github.com/backstage/backstage/pull/16015) + +### New plugin: Linguist + +This plugin lets you see a nice visual representation of the breakdown of languages that a component’s repository is using, similar to the repository information that e.g. GitHub presents. More about that [in the README](https://github.com/backstage/backstage/tree/master/plugins/linguist)! + +Contributed by [@ahhhndre](https://github.com/ahhhndre) in [#14718](https://github.com/backstage/backstage/pull/14718) + +### Lighthouse Backend + +The [Lighthouse plugin](https://github.com/backstage/backstage/tree/master/plugins/lighthouse) now has a [corresponding backend](https://github.com/backstage/backstage/tree/master/plugins/lighthouse-backend). This allows you to conveniently schedule audits from within Backstage. + +Note that as part of this, a `@backstage/plugin-lighthouse-common` package was also created, and types that were common to the frontend and backend were moved there. This means that if you were explicitly importing types from the frontend package, you may see compilation errors and need to add a dependency on the new common package and point your imports there instead. + +Contributed by [@dpfaffenbauer](https://github.com/dpfaffenbauer) in [#15846](https://github.com/backstage/backstage/pull/15846) + +## Security Fixes + +`@backstage/core-components`, `@backstage/catalog-model`, and `@backstage/plugin-catalog-backend`, have all had precautionary validation added, please upgrade to the latest versions of these packages. Special thanks to Philip Olausson ([Programmatic](https://www.programmatic.se/)) who found these issues as part of [Roadie's](https://roadie.io/) regular third-party pen testing. + +## Upgrade path + +We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). + +## Links and References + +Below you can find a list of links and references to help you learn about and start using this new release. + +- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) +- [GitHub repository](https://github.com/backstage/backstage) +- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) + +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.11.0-changelog.md) +- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) + +Sign up for our [newsletter](https://mailchi.mp/spotify/backstage-community) if you want to be informed about what is happening in the world of Backstage. diff --git a/docs/releases/v1.12.0-next.0-changelog.md b/docs/releases/v1.12.0-next.0-changelog.md new file mode 100644 index 0000000000..b7f71b53e6 --- /dev/null +++ b/docs/releases/v1.12.0-next.0-changelog.md @@ -0,0 +1,2364 @@ +# Release v1.12.0-next.0 + +## @backstage/catalog-client@1.4.0-next.0 + +### Minor Changes + +- 8c030b1826: Add `queryEntities` method to `CatalogApi`. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-catalog@1.9.0-next.0 + +### Minor Changes + +- 3f75b7607c: Add ability to pass icon as function to have ability to customize it by search item + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## @backstage/plugin-catalog-backend@1.8.0-next.0 + +### Minor Changes + +- 7f4ea3d360: Add /entities/by-query endpoint returning paginated entities. + + The endpoint supports cursor base pagination and server side sorting of the entities + +### Patch Changes + +- ac8929f2f3: Fix export of `defaultCatalogCollatorEntityTransformer`. +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-react@1.4.0-next.0 + +### Minor Changes + +- 0a5b73b292: Add possibility to re-use EntityPicker for filters with multiple select. + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.11-next.0 + +## @backstage/plugin-linguist-backend@0.2.0-next.0 + +### Minor Changes + +- 4a1c318853: **BREAKING** The linguist-backend `createRouter` now requires that the `tokenManger` is passed to the router. + +### Patch Changes + +- 2ea5782162: Fixed bug in LinguistBackendApi that caused initial batch of entities to be skipped. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-linguist-common@0.1.0 + +## @backstage/plugin-octopus-deploy@0.1.0-next.0 + +### Minor Changes + +- b7fff1171f: Initial version + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-stackstorm@0.1.0-next.0 + +### Minor Changes + +- f3a15259e9: Add StackStorm plugin to Backstage which is interfacing with StackStorm API. It allows users to view workflow executions, packs and actions. For installation and configuration instructions please view [README.md](https://github.com/backstage/backstage/tree/master/plugins/stackstorm/README.md) + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-techdocs@1.6.0-next.0 + +### Minor Changes + +- 3f75b7607c: Add ability to pass icon as function to have ability to customize it by search item + +### Patch Changes + +- f320c299c6: The HTML tag attributes in the documentation content inserted to shadow DOM is preserved to improve accessibility +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## @backstage/app-defaults@1.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-permission-react@0.4.11-next.0 + +## @backstage/backend-app-api@0.4.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-node@0.7.6-next.0 + +## @backstage/backend-common@0.18.3-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-app-api@0.4.1-next.0 + - @backstage/backend-dev-utils@0.1.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/backend-defaults@0.1.8-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-app-api@0.4.1-next.0 + +## @backstage/backend-plugin-api@0.4.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/backend-tasks@0.4.4-next.0 + +### Patch Changes + +- f0685193ef: Added the adapted query to mysql and sqlite3 databases to not returning warning on logs +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/backend-test-utils@0.1.35-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-app-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + +## @backstage/catalog-model@1.2.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/cli@0.22.4-next.0 + +### Patch Changes + +- b4cd145b57: Added a new `migrate package-exports` command that synchronizes package exports fields in all `package.json`s. +- 8bf24946c6: Adjust express dependencies to be the same as the rest of the project +- 17271841de: Updated frontend plugin template to use some more recent features +- Updated dependencies + - @backstage/eslint-plugin@0.1.2-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + +## @backstage/core-app-api@1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + +## @backstage/core-components@0.12.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/version-bridge@1.0.3 + +## @backstage/core-plugin-api@1.4.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + +## @backstage/create-app@0.4.38-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## @backstage/dev-utils@1.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/test-utils@1.2.6-next.0 + - @backstage/app-defaults@1.2.1-next.0 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + +## @backstage/eslint-plugin@0.1.2-next.0 + +### Patch Changes + +- a061c466d6: Fixing a bug that we should check internal dependencies too + +## @backstage/integration-react@1.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## @backstage/repo-tools@0.1.3-next.0 + +### Patch Changes + +- 32a4a05838: Tweaked type dependency check to trim wildcard type imports. +- 6ba8faf22a: The API report generation process is now able to detect and generate reports for additional entry points declared in the package `"exports"` field. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + +## @techdocs/cli@1.3.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/plugin-techdocs-node@1.5.1-next.0 + +## @backstage/test-utils@1.2.6-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.11-next.0 + +## @backstage/plugin-adr@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-adr-common@0.2.7-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## @backstage/plugin-adr-backend@0.3.1-next.0 + +### Patch Changes + +- 8bf24946c6: Adjust express dependencies to be the same as the rest of the project +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.7-next.0 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-adr-common@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/integration@1.4.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-airbrake@0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/test-utils@1.2.6-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/dev-utils@1.0.13-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-airbrake-backend@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-allure@0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-analytics-module-ga@0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-apache-airflow@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + +## @backstage/plugin-api-docs@0.9.1-next.0 + +### Patch Changes + +- 095e755d9b: Add ApiDefinitionDialog component for fast access to the API definition in ProvidedApiCards and ConsumedApiCards. +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-apollo-explorer@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-app-backend@0.3.43-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + +## @backstage/plugin-auth-backend@0.18.1-next.0 + +### Patch Changes + +- 529de8c421: Added authentication provider for Azure Easy Authentication. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + +## @backstage/plugin-auth-node@0.2.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-azure-devops@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-devops-backend@0.3.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-azure-sites-backend@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-badges-backend@0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-bazaar@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/cli@0.22.4-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-bazaar-backend@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + +## @backstage/plugin-bitrise@0.1.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-catalog-backend-module-aws@0.1.17-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- bf611cf019: Fix missing `dependencies` in `package.json` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-kubernetes-common@0.6.1-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.14-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.10-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.8-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.11-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-catalog-backend-module-github@0.2.6-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.14-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.2.2-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- bf611cf019: Fix missing `dependencies` in `package.json` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-defaults@0.1.8-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.2-next.0 + +### Patch Changes + +- a4ebd51352: Revert using `baseUrl` for MS Graph provider as it breaks token retrieval +- 928a12a9b3: Internal refactor of `/alpha` exports. +- 2c234a8932: properly support custom graph api URL +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-common@1.0.12-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-graph@0.2.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-catalog-graphql@0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-import@0.9.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + +## @backstage/plugin-catalog-node@1.3.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-cicd-statistics@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-cicd-statistics@0.1.18-next.0 + +## @backstage/plugin-circleci@0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-cloudbuild@0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-code-climate@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-code-coverage@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-code-coverage-backend@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-codescene@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-config-schema@0.1.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + +## @backstage/plugin-cost-insights@0.12.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-dynatrace@3.0.0-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-entity-feedback@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-entity-feedback-common@0.1.0 + +## @backstage/plugin-entity-feedback-backend@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-entity-feedback-common@0.1.0 + +## @backstage/plugin-entity-validation@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-events-backend@0.2.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-module-aws-sqs@0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-events-backend-module-azure@0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + +## @backstage/plugin-events-backend-module-github@0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-module-gitlab@0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-test-utils@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.4-next.0 + +## @backstage/plugin-events-node@0.2.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + +## @backstage/plugin-explore@0.4.1-next.0 + +### Patch Changes + +- 3f75b7607c: Add ability to pass icon as function to have ability to customize it by search item +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.27-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## @backstage/plugin-explore-backend@0.0.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-explore-react@0.0.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/plugin-explore-common@0.0.1 + +## @backstage/plugin-firehydrant@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-fossa@0.2.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-gcalendar@0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-gcp-projects@0.3.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-git-release-manager@0.3.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## @backstage/plugin-github-actions@0.5.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## @backstage/plugin-github-deployments@0.1.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-github-issues@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## @backstage/plugin-github-pull-requests-board@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## @backstage/plugin-gitops-profiles@0.3.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-gocd@0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-graphiql@0.2.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-graphql-backend@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.19-next.0 + +## @backstage/plugin-graphql-voyager@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-home@0.4.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-ilert@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-jenkins@0.7.15-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-jenkins-common@0.1.14-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-jenkins-backend@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-jenkins-common@0.1.14-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-jenkins-common@0.1.14-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-kafka@0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-kafka-backend@0.2.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-kubernetes@0.7.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-kubernetes-common@0.6.1-next.0 + +## @backstage/plugin-kubernetes-backend@0.9.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-kubernetes-common@0.6.1-next.0 + +## @backstage/plugin-kubernetes-common@0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + +## @backstage/plugin-lighthouse@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-lighthouse-common@0.1.0 + +## @backstage/plugin-lighthouse-backend@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-lighthouse-common@0.1.0 + +## @backstage/plugin-linguist@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-linguist-common@0.1.0 + +## @backstage/plugin-microsoft-calendar@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-newrelic@0.3.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-newrelic-dashboard@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + +## @backstage/plugin-org@0.6.6-next.0 + +### Patch Changes + +- 36bccb5252: Fix small UI-glitch in MemberListCard +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-org-react@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-pagerduty@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-periskop@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-periskop-backend@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-permission-backend@0.5.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + +## @backstage/plugin-permission-node@0.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-permission-react@0.4.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-playlist@0.1.7-next.0 + +### Patch Changes + +- 122a37a880: Internal update to work with newer versions of react-router +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.11-next.0 + - @backstage/plugin-playlist-common@0.1.4 + - @backstage/plugin-search-react@1.5.1-next.0 + +## @backstage/plugin-playlist-backend@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + - @backstage/plugin-playlist-common@0.1.4 + +## @backstage/plugin-proxy-backend@0.2.37-next.0 + +### Patch Changes + +- 3e3eea4bc7: The proxy-backend plugin now supports reviving request bodies that have previously been consumed by an express middleware (e.g. `express.json()`). This is done by setting `reviveConsumedRequestBodies: true` on the proxy `RouterOptions`. In order to preserve the current behavior, the proxy will **not** revive request bodies by default. + + The following is an example of a proxy `createRouter` invocation that revives request bodies: + + ```diff + const router = await createRouter({ + config, + logger, + discovery, + + reviveConsumedRequestBodies: true, + }); + ``` + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-rollbar@0.4.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-rollbar-backend@0.1.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + +## @backstage/plugin-scaffolder@1.11.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-scaffolder-react@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-react@0.4.11-next.0 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + +## @backstage/plugin-scaffolder-backend@1.11.1-next.0 + +### Patch Changes + +- c6c78b4acb: throw error from catalog:fetch scaffolder action when entity is null and optional is false +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.11.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.11.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## @backstage/plugin-scaffolder-common@1.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-node@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + +## @backstage/plugin-scaffolder-react@1.1.1-next.0 + +### Patch Changes + +- c8d78b9ae9: fix bug with `hasErrors` returning false when dealing with empty objects +- 928a12a9b3: Internal refactor of `/alpha` exports. +- cc418d652a: scaffolder/next: Added the ability to get the fields definition in the schema in the validation function +- d4100d0ec4: Fix alignment bug for owners on `TemplateCard` +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + +## @backstage/plugin-search@1.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## @backstage/plugin-search-backend@1.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + - @backstage/plugin-search-backend-node@1.1.4-next.0 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.4-next.0 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-pg@0.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.4-next.0 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-node@1.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-react@1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-sentry@0.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-shortcuts@0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + +## @backstage/plugin-sonarqube@0.6.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- 8ae65a0bc1: Moved imports from `/alpha` to main public exports. +- d3d624e8e8: Show a more appropriate icon if there are no code smells and/or vulnerabilities. +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/plugin-sonarqube-react@0.1.4-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-sonarqube-backend@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-sonarqube-react@0.1.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- 8ae65a0bc1: Moved imports from `/alpha` to main public exports. +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + +## @backstage/plugin-splunk-on-call@0.4.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-stack-overflow@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-home@0.4.32-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## @backstage/plugin-stack-overflow-backend@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-tech-insights@0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10 + +## @backstage/plugin-tech-insights-backend@0.5.9-next.0 + +### Patch Changes + +- f244b58916: Add DB index to improve latency of latest fact query +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/plugin-tech-insights-node@0.4.1-next.0 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/plugin-tech-insights-node@0.4.1-next.0 + +## @backstage/plugin-tech-insights-node@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10 + +## @backstage/plugin-tech-radar@0.6.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.11-next.0 + +### Patch Changes + +- 2b00d8a225: Fix bug/problem with test helper +- Updated dependencies + - @backstage/plugin-techdocs@1.6.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/test-utils@1.2.6-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-search-react@1.5.1-next.0 + +## @backstage/plugin-techdocs-backend@1.5.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-node@1.5.1-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + +## @backstage/plugin-techdocs-node@1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-techdocs-react@1.1.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/version-bridge@1.0.3 + +## @backstage/plugin-todo@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-todo-backend@0.1.40-next.0 + +### Patch Changes + +- 4120513412: todo-backend is now exposed as a plugin which uses the new plugin system +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-user-settings@0.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + +## @backstage/plugin-user-settings-backend@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + +## @backstage/plugin-vault@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## @backstage/plugin-vault-backend@0.2.10-next.0 + +### Patch Changes + +- 66b3a3956b: Ignore the `eslint` error +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-xcmetrics@0.2.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## example-app@0.2.81-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.1-next.0 + - @backstage/cli@0.22.4-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/plugin-techdocs@1.6.0-next.0 + - @backstage/plugin-api-docs@0.9.1-next.0 + - @backstage/plugin-org@0.6.6-next.0 + - @backstage/plugin-stackstorm@0.1.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/plugin-cost-insights@0.12.5-next.0 + - @backstage/plugin-scaffolder@1.11.1-next.0 + - @backstage/plugin-jenkins@0.7.15-next.0 + - @backstage/plugin-playlist@0.1.7-next.0 + - @backstage/plugin-explore@0.4.1-next.0 + - @backstage/plugin-catalog-graph@0.2.28-next.0 + - @backstage/plugin-catalog-import@0.9.6-next.0 + - @backstage/app-defaults@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-airbrake@0.3.16-next.0 + - @backstage/plugin-apache-airflow@0.2.9-next.0 + - @backstage/plugin-azure-devops@0.2.7-next.0 + - @backstage/plugin-azure-sites@0.1.5-next.0 + - @backstage/plugin-badges@0.2.40-next.0 + - @backstage/plugin-circleci@0.3.16-next.0 + - @backstage/plugin-cloudbuild@0.3.16-next.0 + - @backstage/plugin-code-coverage@0.2.9-next.0 + - @backstage/plugin-dynatrace@3.0.0-next.0 + - @backstage/plugin-entity-feedback@0.1.1-next.0 + - @backstage/plugin-gcalendar@0.3.12-next.0 + - @backstage/plugin-gcp-projects@0.3.35-next.0 + - @backstage/plugin-github-actions@0.5.16-next.0 + - @backstage/plugin-gocd@0.1.22-next.0 + - @backstage/plugin-graphiql@0.2.48-next.0 + - @backstage/plugin-home@0.4.32-next.0 + - @backstage/plugin-kafka@0.3.16-next.0 + - @backstage/plugin-kubernetes@0.7.9-next.0 + - @backstage/plugin-lighthouse@0.4.1-next.0 + - @backstage/plugin-linguist@0.1.1-next.0 + - @backstage/plugin-linguist-common@0.1.0 + - @backstage/plugin-microsoft-calendar@0.1.1-next.0 + - @backstage/plugin-newrelic@0.3.34-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.9-next.0 + - @backstage/plugin-pagerduty@0.5.9-next.0 + - @backstage/plugin-permission-react@0.4.11-next.0 + - @backstage/plugin-rollbar@0.4.16-next.0 + - @backstage/plugin-search@1.1.1-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + - @backstage/plugin-sentry@0.5.1-next.0 + - @backstage/plugin-shortcuts@0.3.8-next.0 + - @backstage/plugin-stack-overflow@0.1.12-next.0 + - @backstage/plugin-tech-insights@0.3.8-next.0 + - @backstage/plugin-tech-radar@0.6.2-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.11-next.0 + - @backstage/plugin-todo@0.2.18-next.0 + - @backstage/plugin-user-settings@0.7.1-next.0 + - @internal/plugin-catalog-customized@0.0.8-next.0 + +## example-backend@0.2.81-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-todo-backend@0.1.40-next.0 + - @backstage/plugin-scaffolder-backend@1.11.1-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/plugin-tech-insights-backend@0.5.9-next.0 + - @backstage/plugin-linguist-backend@0.2.0-next.0 + - @backstage/plugin-adr-backend@0.3.1-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/plugin-techdocs-backend@1.5.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-events-backend@0.2.4-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/plugin-app-backend@0.3.43-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/plugin-proxy-backend@0.2.37-next.0 + - @backstage/plugin-auth-backend@0.18.1-next.0 + - @backstage/plugin-badges-backend@0.1.37-next.0 + - @backstage/plugin-code-coverage-backend@0.2.9-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.1-next.0 + - @backstage/plugin-jenkins-backend@0.1.33-next.0 + - @backstage/plugin-kubernetes-backend@0.9.4-next.0 + - @backstage/plugin-lighthouse-backend@0.1.1-next.0 + - @backstage/plugin-playlist-backend@0.2.6-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.11-next.0 + - example-app@0.2.81-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-azure-devops-backend@0.3.22-next.0 + - @backstage/plugin-azure-sites-backend@0.1.5-next.0 + - @backstage/plugin-explore-backend@0.0.5-next.0 + - @backstage/plugin-graphql-backend@0.1.33-next.0 + - @backstage/plugin-kafka-backend@0.2.36-next.0 + - @backstage/plugin-permission-backend@0.5.18-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + - @backstage/plugin-rollbar-backend@0.1.40-next.0 + - @backstage/plugin-search-backend@1.2.4-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.4-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.4-next.0 + - @backstage/plugin-search-backend-node@1.1.4-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.27-next.0 + - @backstage/plugin-tech-insights-node@0.4.1-next.0 + +## example-backend-next@0.0.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-todo-backend@0.1.40-next.0 + - @backstage/plugin-scaffolder-backend@1.11.1-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-defaults@0.1.8-next.0 + - @backstage/plugin-app-backend@0.3.43-next.0 + +## e2e-test@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.4.38-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + +## techdocs-cli-embedded-app@0.2.80-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.4-next.0 + - @backstage/plugin-techdocs@1.6.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/test-utils@1.2.6-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + - @backstage/app-defaults@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + +## @internal/plugin-catalog-customized@0.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + +## @internal/plugin-todo-list@1.0.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## @internal/plugin-todo-list-backend@1.0.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 diff --git a/docs/releases/v1.2.0.md b/docs/releases/v1.2.0.md index b2d50a09a2..3751217ae4 100644 --- a/docs/releases/v1.2.0.md +++ b/docs/releases/v1.2.0.md @@ -60,7 +60,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.2.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) diff --git a/docs/releases/v1.3.0.md b/docs/releases/v1.3.0.md index 8014e0a121..96aa88a05e 100644 --- a/docs/releases/v1.3.0.md +++ b/docs/releases/v1.3.0.md @@ -58,7 +58,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.3.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap), and [Plugins](https://backstage.io/plugins) diff --git a/docs/releases/v1.4.0.md b/docs/releases/v1.4.0.md index 20b1739f4a..9a178ea8ff 100644 --- a/docs/releases/v1.4.0.md +++ b/docs/releases/v1.4.0.md @@ -63,7 +63,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.4.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) diff --git a/docs/releases/v1.5.0.md b/docs/releases/v1.5.0.md index ca4ae11185..d7bbfa62a2 100644 --- a/docs/releases/v1.5.0.md +++ b/docs/releases/v1.5.0.md @@ -53,7 +53,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.5.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) diff --git a/docs/releases/v1.6.0.md b/docs/releases/v1.6.0.md index a1ece6bf94..417491c1e4 100644 --- a/docs/releases/v1.6.0.md +++ b/docs/releases/v1.6.0.md @@ -53,7 +53,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.6.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) diff --git a/docs/releases/v1.7.0.md b/docs/releases/v1.7.0.md index cb87ead4c9..56eda8b20d 100644 --- a/docs/releases/v1.7.0.md +++ b/docs/releases/v1.7.0.md @@ -114,7 +114,7 @@ start using this new release. - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.7.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), diff --git a/docs/releases/v1.8.0.md b/docs/releases/v1.8.0.md index cbe16a0cba..78cc5ab7cd 100644 --- a/docs/releases/v1.8.0.md +++ b/docs/releases/v1.8.0.md @@ -71,7 +71,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.8.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) diff --git a/docs/releases/v1.9.0.md b/docs/releases/v1.9.0.md index 53a1769108..a677acb643 100644 --- a/docs/releases/v1.9.0.md +++ b/docs/releases/v1.9.0.md @@ -78,7 +78,7 @@ Below you can find a list of links and references to help you learn about and st - [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) -- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.9.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index ed550f1801..8cd5ce99a9 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -21,8 +21,8 @@ instance or cluster. With infrastructure defined as code or data (Terraform, AWS CloudFormation, etc.), you may have database credentials which lack permissions to create new databases or you do not have control over the database names. In these -instances, you can set the database connection configuration on a [per plugin basis] -(#connection-configuration-per-plugin). +instances, you can set the database connection configuration on a +[per plugin basis](#connection-configuration-per-plugin). Backstage supports all of these use cases with the `DatabaseManager` provided by `@backstage/backend-common`. We will now cover how to use and configure @@ -37,7 +37,7 @@ package. If you intend to use both PostgreSQL and SQLite, you can install both of them. ```bash -# From the Backstage root directory +# From your Backstage root directory # install pg if you need PostgreSQL yarn add --cwd packages/backend pg diff --git a/microsite-next/docusaurus.config.js b/microsite-next/docusaurus.config.js index 2b37ae20b2..b3d1d16af1 100644 --- a/microsite-next/docusaurus.config.js +++ b/microsite-next/docusaurus.config.js @@ -13,6 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// @ts-check + +/** @type {import('@docusaurus/types').Config} */ module.exports = { title: 'Backstage Software Catalog and Developer Platform', tagline: 'An open platform for building developer portals', @@ -40,10 +44,9 @@ module.exports = { presets: [ [ '@docusaurus/preset-classic', + /** @type {import('@docusaurus/preset-classic').Options} */ { docs: { - showLastUpdateAuthor: true, - showLastUpdateTime: true, editUrl: 'https://github.com/backstage/backstage/edit/master/docs/', path: '../docs', sidebarPath: 'sidebars.json', @@ -60,138 +63,191 @@ module.exports = { }, ], ], - plugins: [], - themeConfig: { - navbar: { - title: 'Backstage Software Catalog and Developer Platform', - items: [ - { - href: 'https://github.com/backstage/backstage', - label: 'GitHub', - position: 'left', + webpack: { + jsLoader: isServer => ({ + loader: require.resolve('swc-loader'), + options: { + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + target: 'es2017', }, - { - to: 'docs/overview/what-is-backstage', - label: 'Docs', - position: 'left', + module: { + type: isServer ? 'commonjs' : 'es6', }, - { - to: '/plugins', - label: 'Plugins', - position: 'left', - }, - { - to: '/blog', - label: 'Blog', - position: 'left', - }, - { - to: 'docs/releases/v1.8.0', - label: 'Releases', - position: 'left', - }, - { - to: '/demos', - label: 'Demos', - position: 'left', - }, - { - to: '/community', - label: 'Community', - position: 'left', - }, - ], - }, - image: 'img/sharing-opengraph.png', - footer: { - links: [ - { - items: [ - { - html: '<a href="/"><h2 class="footerLogo"></h2></a>', - }, - ], - }, - { - title: 'Docs', - items: [ - { - label: 'What is Backstage?', - to: 'docs/overview/what-is-backstage', - }, - { - label: 'Getting started', - to: 'docs/getting-started/', - }, - { - label: 'Software Catalog', - to: 'docs/features/software-catalog/software-catalog-overview', - }, - { - label: 'Create a Plugin', - to: 'docs/plugins/create-a-plugin', - }, - { - label: 'Designing for Backstage', - to: 'docs/dls/design', - }, - ], - }, - { - title: 'Community', - items: [ - { - label: 'Support chatroom', - to: 'https://discord.gg/MUpMjP2', - }, - { - label: 'Contributing', - to: 'https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md', - }, - { - label: 'Adopting', - to: 'https://backstage.spotify.com', - }, - { - label: 'Subscribe to our newsletter', - to: 'https://mailchi.mp/spotify/backstage-community', - }, - { - label: 'CNCF Incubation', - to: 'https://www.cncf.io/projects/', - }, - ], - }, - { - title: 'More', - items: [ - { - label: 'Open Source @ Spotify', - to: 'https://spotify.github.io/', - }, - { - label: 'Spotify Engineering Blog', - to: 'https://engineering.atspotify.com/', - }, - { - label: 'Spotify for Developers', - to: 'https://developer.spotify.com/', - }, - { - label: 'Github', - to: 'https://github.com/backstage/', - }, - ], - }, - ], - copyright: - '<p style="text-align:center"><a href="https://spotify.github.io/">Made with ❤️ at Spotify</a></p><p class="copyright">Copyright © 2022 Backstage Project Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage</p>', - }, - algolia: { - appId: 'AZYC4ZFNTN', - apiKey: '7dbd2089b0d445ee0d87db71abfc794f', - indexName: 'backstage', - searchParameters: {}, - }, + }, + }), }, + plugins: [ + 'docusaurus-plugin-sass', + () => ({ + configureWebpack() { + return { + module: { + rules: [ + { + test: /\.ya?ml$/, + use: 'yaml-loader', + }, + ], + }, + }; + }, + }), + [ + '@docusaurus/plugin-client-redirects', + { + redirects: [ + { + from: '/docs', + to: '/docs/overview/what-is-backstage', + }, + ], + }, + ], + ], + themeConfig: + /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ + { + colorMode: { + defaultMode: 'dark', + disableSwitch: true, + }, + navbar: { + logo: { + alt: 'Backstage Software Catalog and Developer Platform', + src: 'img/logo.svg', + }, + items: [ + { + href: 'https://github.com/backstage/backstage', + label: 'GitHub', + position: 'left', + }, + { + to: 'docs/overview/what-is-backstage', + label: 'Docs', + position: 'left', + }, + { + to: '/plugins', + label: 'Plugins', + position: 'left', + }, + { + to: '/blog', + label: 'Blog', + position: 'left', + }, + { + to: 'docs/releases/v1.11.0', + label: 'Releases', + position: 'left', + }, + { + to: '/demos', + label: 'Demos', + position: 'left', + }, + { + to: '/community', + label: 'Community', + position: 'left', + }, + ], + }, + image: 'img/sharing-opengraph.png', + footer: { + links: [ + { + items: [ + { + html: '<a href="/"><h2 class="footerLogo"></h2></a>', + }, + ], + }, + { + title: 'Docs', + items: [ + { + label: 'What is Backstage?', + to: 'docs/overview/what-is-backstage', + }, + { + label: 'Getting started', + to: 'docs/getting-started/', + }, + { + label: 'Software Catalog', + to: 'docs/features/software-catalog/software-catalog-overview', + }, + { + label: 'Create a Plugin', + to: 'docs/plugins/create-a-plugin', + }, + { + label: 'Designing for Backstage', + to: 'docs/dls/design', + }, + ], + }, + { + title: 'Community', + items: [ + { + label: 'Support chatroom', + to: 'https://discord.gg/backstage-687207715902193673', + }, + { + label: 'Contributing', + to: 'https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md', + }, + { + label: 'Adopting', + to: 'https://backstage.spotify.com', + }, + { + label: 'Subscribe to our newsletter', + to: 'https://mailchi.mp/spotify/backstage-community', + }, + { + label: 'CNCF Incubation', + to: 'https://www.cncf.io/projects/', + }, + ], + }, + { + title: 'More', + items: [ + { + label: 'Open Source @ Spotify', + to: 'https://spotify.github.io/', + }, + { + label: 'Spotify Engineering Blog', + to: 'https://engineering.atspotify.com/', + }, + { + label: 'Spotify for Developers', + to: 'https://developer.spotify.com/', + }, + { + label: 'Github', + to: 'https://github.com/backstage/', + }, + ], + }, + ], + copyright: + '<p style="text-align:center"><a href="https://spotify.github.io/">Made with ❤️ at Spotify</a></p><p class="copyright">Copyright © 2022 Backstage Project Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage</p>', + }, + algolia: { + apiKey: '1f0ba86672ccfc3576faa94583e5b318', + indexName: 'crawler_Backstage Docusaurus 2', + appId: 'JCMFNHCHI8', + searchParameters: {}, + }, + }, }; diff --git a/microsite-next/package.json b/microsite-next/package.json index 478fce0196..cb71d5a25a 100644 --- a/microsite-next/package.json +++ b/microsite-next/package.json @@ -17,17 +17,27 @@ "docusaurus": "docusaurus" }, "devDependencies": { + "@docusaurus/module-type-aliases": "2.3.1", "@spotify/prettier-config": "^14.0.0", + "@tsconfig/docusaurus": "^1.0.6", + "@types/webpack-env": "^1.18.0", "js-yaml": "^4.1.0", "prettier": "^2.6.2", - "replace": "^1.2.2" + "replace": "^1.2.2", + "typescript": "^4.9.4", + "yaml-loader": "^0.8.0" }, "prettier": "@spotify/prettier-config", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/preset-classic": "2.2.0", + "@docusaurus/core": "2.3.1", + "@docusaurus/plugin-client-redirects": "^2.3.1", + "@docusaurus/preset-classic": "2.3.1", + "@swc/core": "^1.3.36", "clsx": "^1.1.1", + "docusaurus-plugin-sass": "^0.2.3", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "sass": "^1.57.1", + "swc-loader": "^0.2.3" } } diff --git a/microsite-next/sidebars.json b/microsite-next/sidebars.json index 67c4209976..f489771fa0 100644 --- a/microsite-next/sidebars.json +++ b/microsite-next/sidebars.json @@ -1,6 +1,9 @@ { "releases": { "Release Notes": [ + "releases/v1.11.0", + "releases/v1.10.0", + "releases/v1.9.0", "releases/v1.8.0", "releases/v1.7.0", "releases/v1.6.0", @@ -85,7 +88,8 @@ "features/kubernetes/installation", "features/kubernetes/configuration", "features/kubernetes/authentication", - "features/kubernetes/troubleshooting" + "features/kubernetes/troubleshooting", + "features/kubernetes/proxy" ] }, { @@ -101,6 +105,7 @@ "features/software-templates/writing-custom-actions", "features/software-templates/writing-custom-field-extensions", "features/software-templates/writing-custom-step-layouts", + "features/software-templates/testing-scaffolder-alpha", "features/software-templates/migrating-from-v1beta2-to-v1beta3" ] }, @@ -199,7 +204,8 @@ "label": "GitLab", "items": [ "integrations/gitlab/locations", - "integrations/gitlab/discovery" + "integrations/gitlab/discovery", + "integrations/gitlab/org" ] }, { @@ -237,8 +243,7 @@ "plugins/proxying", "plugins/backend-plugin", "plugins/call-existing-api", - "plugins/url-reader", - "plugins/new-backend-system" + "plugins/url-reader" ] }, { @@ -361,6 +366,43 @@ "architecture-decisions/adrs-adr012", "architecture-decisions/adrs-adr013" ], - "FAQ": ["FAQ"] + "FAQ": ["FAQ"], + "Experimental Backend System": [ + "backend-system/index", + { + "type": "category", + "label": "Architecture", + "items": [ + "backend-system/architecture/index", + "backend-system/architecture/services", + "backend-system/architecture/plugins", + "backend-system/architecture/extension-points", + "backend-system/architecture/modules", + "backend-system/architecture/naming-patterns" + ] + }, + { + "type": "category", + "label": "Building Backends", + "items": [ + "backend-system/building-backends/index", + "backend-system/building-backends/migrating" + ] + }, + { + "type": "category", + "label": "Building Plugins & Modules", + "items": [ + "backend-system/building-plugins-and-modules/index", + "backend-system/building-plugins-and-modules/testing", + "backend-system/building-plugins-and-modules/migrating" + ] + }, + { + "type": "category", + "label": "Core Services", + "items": ["backend-system/core-services/index"] + } + ] } } diff --git a/microsite-next/src/components/bannerSection/bannerSection.module.scss b/microsite-next/src/components/bannerSection/bannerSection.module.scss new file mode 100644 index 0000000000..2beabf9738 --- /dev/null +++ b/microsite-next/src/components/bannerSection/bannerSection.module.scss @@ -0,0 +1,35 @@ +.bannerSection { + display: block; + + &:global(.greyBackground) { + background: linear-gradient(90.49deg, #121212 15.36%, #282828 70.44%); + } + + &:global(.greenGradientBackground) { + background: linear-gradient(70.44deg, #121212 55%, #5d817b); + } + + &:global(.greenBottomGradientBackground) { + background: linear-gradient( + 178.64deg, + hsla(0, 0%, 100%, 0) 57.61%, + hsla(0, 0%, 100%, 0.17) 127.71% + ), + linear-gradient( + 144.35deg, + rgba(98, 197, 179, 0) 56.68%, + rgba(98, 197, 179, 0.59) 109.25% + ), + linear-gradient( + 192.29deg, + rgba(155, 240, 225, 0) 54.17%, + rgba(155, 240, 225, 0.67) 137.34% + ); + } + + &:global(.greenCallToActionGradientBackground) { + background: linear-gradient(87.29deg, #9bf0e1 17.71%, #36baa2 80.85%); + + color: var(--ifm-color-gray-900); + } +} diff --git a/microsite-next/src/components/bannerSection/bannerSection.tsx b/microsite-next/src/components/bannerSection/bannerSection.tsx new file mode 100644 index 0000000000..64c8d246c8 --- /dev/null +++ b/microsite-next/src/components/bannerSection/bannerSection.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; +import React, { PropsWithChildren } from 'react'; + +import bannerStyles from './bannerSection.module.scss'; + +export type IBannerSectionProps = PropsWithChildren<{ + className?: string; + greyBackground?: boolean; + greenGradientBackground?: boolean; + greenBottomGradientBackground?: boolean; + greenCallToActionGradientBackground?: boolean; +}>; + +export const BannerSection = ({ + children, + className, + greyBackground = false, + greenGradientBackground = false, + greenBottomGradientBackground = false, + greenCallToActionGradientBackground = false, +}: IBannerSectionProps) => ( + <section + className={clsx(bannerStyles.bannerSection, className, { + greyBackground, + greenGradientBackground, + greenBottomGradientBackground, + greenCallToActionGradientBackground, + })} + > + <div className="container padding-horiz--lg padding-vert--xl"> + {children} + </div> + </section> +); diff --git a/microsite-next/src/components/bannerSection/bannerSectionGrid.module.scss b/microsite-next/src/components/bannerSection/bannerSectionGrid.module.scss new file mode 100644 index 0000000000..c57b82ac57 --- /dev/null +++ b/microsite-next/src/components/bannerSection/bannerSectionGrid.module.scss @@ -0,0 +1,20 @@ +$desktopUpBreakpoint: 997px; + +.sectionGridContainer { + gap: 2rem; + display: grid; + + :global(.gridHeader) { + grid-column: 1 / -1; + } +} + +.gridContainer { + gap: 2rem; + display: grid; + align-items: center; + + @media (min-width: $desktopUpBreakpoint) { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + } +} diff --git a/microsite-next/src/components/bannerSection/bannerSectionGrid.tsx b/microsite-next/src/components/bannerSection/bannerSectionGrid.tsx new file mode 100644 index 0000000000..dc9853d1fe --- /dev/null +++ b/microsite-next/src/components/bannerSection/bannerSectionGrid.tsx @@ -0,0 +1,32 @@ +import clsx from 'clsx'; +import React, { PropsWithChildren, ReactNode } from 'react'; + +import bannerGridStyles from './bannerSectionGrid.module.scss'; + +export type IBannerSectionColumnsProps = PropsWithChildren<{ + header?: ReactNode; + className?: string; +}>; + +export const BannerSectionGrid = ({ + header, + children, + className, +}: IBannerSectionColumnsProps) => { + return ( + <div className={clsx(bannerGridStyles.sectionGridContainer, className)}> + {header && <div className="gridHeader">{header}</div>} + + <div className={bannerGridStyles.gridContainer}> + {Array.isArray(children) + ? children.map((child, index) => + React.cloneElement(child, { + key: index, + className: clsx(child.props.className), + }), + ) + : children} + </div> + </div> + ); +}; diff --git a/microsite-next/src/components/contentBlock/contentBlock.module.scss b/microsite-next/src/components/contentBlock/contentBlock.module.scss new file mode 100644 index 0000000000..9a34c55390 --- /dev/null +++ b/microsite-next/src/components/contentBlock/contentBlock.module.scss @@ -0,0 +1,30 @@ +.contentBlock { + height: 100%; + + gap: 1.5rem; + display: grid; + align-items: center; + + > *, + h1, + h2 { + margin: 0; + padding: 0; + } + + :global(.contentBlockTitle) { + align-self: start; + } + + :global(.bulletLine) { + width: 50px; + margin-bottom: 0.5rem; + } + + :global(.actionButtons) { + gap: 1rem; + display: grid; + align-self: end; + grid-template-columns: repeat(auto-fit, minmax(100px, auto)); + } +} diff --git a/microsite-next/src/components/contentBlock/contentBlock.tsx b/microsite-next/src/components/contentBlock/contentBlock.tsx new file mode 100644 index 0000000000..aae065db44 --- /dev/null +++ b/microsite-next/src/components/contentBlock/contentBlock.tsx @@ -0,0 +1,53 @@ +import Link from '@docusaurus/Link'; +import clsx from 'clsx'; +import React, { FC, PropsWithChildren, ReactNode } from 'react'; + +import contentBlockStyles from './contentBlock.module.scss'; + +export interface ContentBlockActionButtonProps { + label: string; + link: string; +} + +export type IContentBlockProps = PropsWithChildren<{ + title?: ReactNode; + className?: string; + topImgSrc?: string; + hasBulletLine?: boolean; + actionButtons?: ContentBlockActionButtonProps[]; +}>; + +export const ContentBlock = ({ + children, + title, + className, + topImgSrc, + hasBulletLine, + actionButtons, +}: IContentBlockProps) => ( + <div className={clsx(className, contentBlockStyles.contentBlock)}> + {topImgSrc && <img src={topImgSrc} alt={topImgSrc} />} + + <div className="contentBlockTitle"> + {hasBulletLine && <div className="bulletLine" />} + + {title && React.isValidElement(title) ? title : <h2>{title}</h2>} + </div> + + {children && React.isValidElement(children) ? children : <p>{children}</p>} + + {actionButtons && ( + <div className="actionButtons"> + {actionButtons.map(({ link, label }, index) => ( + <Link + key={index} + className="button button--primary button--lg" + to={link} + > + {label} + </Link> + ))} + </div> + )} + </div> +); diff --git a/microsite-next/src/css/customTheme.css b/microsite-next/src/css/customTheme.css index 3b71d51d62..1a39467516 100644 --- a/microsite-next/src/css/customTheme.css +++ b/microsite-next/src/css/customTheme.css @@ -25,7 +25,7 @@ background-color: white; } -/* Utility API Styling */ +/* #region Utility API Styling */ .textGreen { color: #82b366; } @@ -44,4 +44,15 @@ .textAlignCenter { text-align: center; } -/* End of Utility API Styling */ +/* #endregion */ + +/* #region Utility component styles */ +.bulletLine { + height: 3px; + background: linear-gradient( + 89.75deg, + #9bf0e1 -1.5%, + rgba(155, 240, 225, 0) 111.48% + ); +} +/* #endregion */ diff --git a/microsite-next/src/pages/community/community.module.scss b/microsite-next/src/pages/community/community.module.scss new file mode 100644 index 0000000000..3f829b0100 --- /dev/null +++ b/microsite-next/src/pages/community/community.module.scss @@ -0,0 +1,28 @@ +.communityPage { + // --ifm-spacing-horizontal: 2rem; + + font-size: 1.25rem; + + h1 { + font-size: 54px; + line-height: 56px; + word-break: break-word; + } + + p { + text-align: justify; + } +} + +.listContainer { + height: 100%; + align-items: stretch !important; + grid-template-rows: auto 1fr; + + ul { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + } +} diff --git a/microsite-next/src/pages/community/index.tsx b/microsite-next/src/pages/community/index.tsx new file mode 100644 index 0000000000..d334b04ee2 --- /dev/null +++ b/microsite-next/src/pages/community/index.tsx @@ -0,0 +1,270 @@ +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import { BannerSection } from '@site/src/components/bannerSection/bannerSection'; +import { BannerSectionGrid } from '@site/src/components/bannerSection/bannerSectionGrid'; +import Layout from '@theme/Layout'; +import { clsx } from 'clsx'; +import React from 'react'; + +import { ContentBlock } from '../../components/contentBlock/contentBlock'; +import communityStyles from './community.module.scss'; + +interface ICollectionItem { + title?: string; + content: React.ReactNode; + label: string; + link: string; +} + +const Community = () => { + const { siteConfig } = useDocusaurusContext(); + + //#region Collection Data + const communityListItems: ICollectionItem[] = [ + { + content: 'Chat and get support on our', + label: 'Discord', + link: 'https://discord.gg/backstage-687207715902193673', + }, + { + content: 'Get into contributing with the', + label: 'Good First Issues', + link: 'https://github.com/backstage/backstage/contribute', + }, + { + content: 'Subscribe to the', + label: 'Community newsletter', + link: 'https://info.backstage.spotify.com/newsletter_subscribe', + }, + { + content: 'Join the', + label: 'Twitter community', + link: 'https://twitter.com/i/communities/1494019781716062215', + }, + ]; + + const officialInitiatives: ICollectionItem[] = [ + { + title: 'Community sessions', + content: + 'Maintainers and adopters meet monthly to share updates, demos, and ideas. Yep, all sessions are recorded!', + link: '/on-demand', + label: 'Join a session', + }, + { + title: 'Newsletter', + content: + "The official monthly Backstage newsletter. Don't miss the latest news from your favorite project!", + link: 'https://info.backstage.spotify.com/newsletter_subscribe', + label: 'Subscribe', + }, + { + title: 'Contributor Spotlight', + content: + "A recognition for valuable community work. Nominate contributing members for their efforts! We'll put them in the spotlight ❤️", + link: '/nominate', + label: 'Nominate now', + }, + ]; + + const communityInitiatives: ICollectionItem[] = [ + { + title: 'Open Mic Meetup', + content: ( + <> + A casual get together of Backstage users sharing their experiences and + helping each other. Hosted by{' '} + <Link to="https://roadie.io/">Roadie.io</Link> and{' '} + <Link to="https://frontside.com/">Frontside Software</Link>. + </> + ), + link: 'https://backstage-openmic.com/', + label: 'Learn more', + }, + { + title: 'Backstage Weekly Newsletter', + content: ( + <> + A weekly newsletter with news, updates and things community from your + friends at <Link to="https://roadie.io/">Roadie.io</Link>. + </> + ), + link: 'https://roadie.io/backstage-weekly/', + label: 'Learn more', + }, + ]; + + const trainingNCertifications: ICollectionItem[] = [ + { + title: 'Open Mic Meetup', + content: + 'This is a course produced and curated by the Linux Foundation. This course introduces you to Backstage and how to get started with the project.', + link: 'https://training.linuxfoundation.org/training/introduction-to-backstage-developer-portals-made-easy-lfs142x/', + label: 'Learn more', + }, + ]; + + const partners: { name: string; url: string; logo: string }[] = [ + { + name: 'Frontside Software', + url: 'https://frontside.com/backstage/', + logo: 'img/partner-logo-frontside.png', + }, + { + name: 'Roadie', + url: 'https://roadie.io/', + logo: 'img/partner-logo-roadie.png', + }, + { + name: 'ThoughtWorks', + url: 'https://www.thoughtworks.com', + logo: 'img/partner-logo-thoughtworks.png', + }, + { + name: 'VMWare', + url: 'https://www.vmware.com', + logo: 'img/partner-logo-vmware.png', + }, + ]; + //#endregion + + return ( + <Layout> + <div className={clsx(communityStyles.communityPage)}> + <BannerSection greyBackground> + <BannerSectionGrid> + <ContentBlock + className="padding-right--xl" + title={<h1>Backstage Community</h1>} + > + Join the vibrant community around Backstage through social media + and different meetups. To ensure that you have a welcoming + environment, we follow the + <Link to="https://github.com/cncf/foundation/blob/master/code-of-conduct.md"> + {' '} + CNCF Code of Conduct{' '} + </Link> + in everything we do. + </ContentBlock> + + <ContentBlock + className={clsx( + 'padding-left--xl', + communityStyles.listContainer, + )} + title="Get started in our community!" + > + <ul> + {communityListItems.map( + ({ content: text, link, label }, index) => ( + <li key={index}> + <p className="margin-bottom--none"> + {text} <Link to={link}>{label}</Link> + </p> + </li> + ), + )} + </ul> + </ContentBlock> + </BannerSectionGrid> + </BannerSection> + + <BannerSection greenBottomGradientBackground> + <BannerSectionGrid + header={ + <> + <h2 className="text--primary">Offical Backstage initiatives</h2> + + <h1>Stay tuned to the latest developments</h1> + </> + } + > + {officialInitiatives.map( + ({ title, content, link, label }, index) => ( + <ContentBlock + key={index} + title={title} + hasBulletLine + actionButtons={[ + { + link, + label, + }, + ]} + > + {content} + </ContentBlock> + ), + )} + </BannerSectionGrid> + </BannerSection> + + <BannerSection greyBackground> + <BannerSectionGrid + header={<h2 className="text--primary">Community initiatives</h2>} + > + {communityInitiatives.map( + ({ title, content, link, label }, index) => ( + <ContentBlock + key={index} + title={title} + hasBulletLine + actionButtons={[ + { + link, + label, + }, + ]} + > + <p>{content}</p> + </ContentBlock> + ), + )} + </BannerSectionGrid> + </BannerSection> + + <BannerSection> + <BannerSectionGrid + header={ + <h2 className="text--primary">Trainings and Certifications</h2> + } + > + {trainingNCertifications.map( + ({ title, content, link, label }, index) => ( + <ContentBlock + key={index} + title={title} + hasBulletLine + actionButtons={[ + { + link, + label, + }, + ]} + > + <p>{content}</p> + </ContentBlock> + ), + )} + </BannerSectionGrid> + </BannerSection> + + <BannerSection greyBackground> + <BannerSectionGrid + header={<h2 className="text--primary">Commercial Partners</h2>} + > + {partners.map(({ name, url, logo }, index) => ( + <div key={index}> + <Link to={url}> + <img src={`${siteConfig.baseUrl}${logo}`} alt={name} /> + </Link> + </div> + ))} + </BannerSectionGrid> + </BannerSection> + </div> + </Layout> + ); +}; + +export default Community; diff --git a/microsite-next/src/pages/demos/demos.module.scss b/microsite-next/src/pages/demos/demos.module.scss new file mode 100644 index 0000000000..b99874297e --- /dev/null +++ b/microsite-next/src/pages/demos/demos.module.scss @@ -0,0 +1,24 @@ +.communityPage { + // --ifm-spacing-horizontal: 2rem; + + font-size: 1.25rem; + + h1 { + font-size: 54px; + line-height: 56px; + word-break: break-word; + } +} + +.banner { + p { + flex: 1; + text-align: justify; + } + + iframe { + width: 100%; + height: 100%; + min-height: 500px; + } +} diff --git a/microsite-next/src/pages/demos/index.tsx b/microsite-next/src/pages/demos/index.tsx new file mode 100644 index 0000000000..2ca77e5c89 --- /dev/null +++ b/microsite-next/src/pages/demos/index.tsx @@ -0,0 +1,206 @@ +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import { BannerSection } from '@site/src/components/bannerSection/bannerSection'; +import { BannerSectionGrid } from '@site/src/components/bannerSection/bannerSectionGrid'; +import { ContentBlock } from '@site/src/components/contentBlock/contentBlock'; +import Layout from '@theme/Layout'; +import { clsx } from 'clsx'; +import React from 'react'; + +import demosStyles from './demos.module.scss'; + +interface IDemoItem { + title: string; + content: React.ReactNode; + actionItemLink?: string; + media: { + type: 'image' | 'video'; + link: string; + }; +} + +const Demos = () => { + const { siteConfig } = useDocusaurusContext(); + + //#region Collection Data + const demoItems: IDemoItem[] = [ + { + title: 'See us in action', + content: ( + <> + <p> + Watch the videos below to get an introduction to Backstage and to + see how we use different plugins to customize{' '} + <Link to="https://engineering.atspotify.com/2020/04/21/how-we-use-backstage-at-spotify/"> + our internal version of Backstage at Spotify + </Link> + </p> + <p> + To see how other companies have already started using Backstage, + watch these presentations from{' '} + <Link to="https://youtu.be/rRphwXeq33Q?t=1508">Expedia</Link>,{' '} + <Link to="https://youtu.be/6sg5uMCLxTA?t=153">Zalando</Link>, and{' '} + <Link to="https://youtu.be/UZTVjv-AvZA?t=188">TELUS</Link>. For + more, join our{' '} + <Link to="https://github.com/backstage/community"> + Community Sessions + </Link> + </p> + <p> + To explore the UI and basic features of Backstage firsthand, go to:{' '} + <Link to="https://demo.backstage.io">demo.backstage.io</Link>. (Tip:{' '} + click “All” to view all the example components in the software + catalog.) + </p> + </> + ), + media: { + type: 'image', + link: `${siteConfig.baseUrl}img/demo-screen.png`, + }, + }, + { + title: 'Introduction to Backstage', + content: + 'Backstage is an open source platform for building developer portals. We’ve been using our homegrown version at Spotify for years — so it’s already packed with features. (We have over 120 internal plugins, built by 60 different teams.) In this live demo recording, Stefan Ålund, product manager for Backstage, tells the origin story of Backstage and gives you a tour of how we use it here at Spotify.', + actionItemLink: 'https://www.youtube.com/watch?v=1XtJ5FAOjPk', + media: { + type: 'video', + link: 'https://www.youtube.com/embed/1XtJ5FAOjPk', + }, + }, + { + title: 'Control cloud costs', + content: ( + <> + <p> + How do you control cloud costs while maintaining the speed and + independence of your development teams? With the{' '} + <Link to="https://backstage.io/plugins">Cost Insights plugin</Link>{' '} + for Backstage, managing cloud costs becomes just another part of an + engineer’s daily development process. They get a clear view of their + spending — and can decide for themselves how they want to optimize + it. Learn more about the{' '} + <Link to="https://backstage.io/blog/2020/10/22/cost-insights-plugin"> + Cost Insights plugin + </Link> + </p> + </> + ), + actionItemLink: 'https://youtu.be/YLAd5hdXR_Q', + media: { + type: 'video', + link: 'https://www.youtube.com/embed/YLAd5hdXR_Q', + }, + }, + { + title: 'Make documentation easy', + content: ( + <> + <p> + Documentation! Everyone needs it, no one wants to create it, and no + one can ever find it. Backstage follows a “docs like code” approach: + you write documentation in Markdown files right alongside your code. + This makes documentation easier to create, maintain, find — and, you + know, actually use. This demo video showcases Spotify’s internal + version of TechDocs. Learn more about{' '} + <Link to="https://backstage.io/blog/2020/09/08/announcing-tech-docs"> + TechDocs + </Link> + . + </p> + </> + ), + actionItemLink: 'https://youtu.be/mOLCgdPw1iA', + media: { + type: 'video', + link: 'https://www.youtube.com/embed/mOLCgdPw1iA', + }, + }, + { + title: 'Manage your tech health', + content: + 'Instead of manually updating a spreadsheet, what if you had a beautiful dashboard that could give you an instant, interactive picture of your entire org’s tech stack? That’s how we do it at Spotify. With our Tech Insights plugin for Backstage, anyone at Spotify can see which version of which software anyone else at Spotify is using — and a whole a lot more. From managing migrations to fighting tech entropy, Backstage makes managing our tech health actually kind of pleasant.', + actionItemLink: + 'https://www.youtube.com/watch?v=K3xz6VAbgH8&list=PLf1KFlSkDLIBtMGwRDfaVlKMqTMrjD2yO&index=6', + media: { + type: 'video', + link: 'https://www.youtube.com/embed/K3xz6VAbgH8', + }, + }, + { + title: 'Create a microservice', + content: + 'You’re a Spotify engineer about to build a new microservice (or any component) using Spring Boot. Where do you start? Search for a quick start guide online? Create an empty repo on GitHub? Copy and paste an old project? Nope. Just go to Backstage, and you’ll be up and running in two minutes — with a “Hello World” app, CI, and documentation all automatically set up and configured in a standardized way.', + actionItemLink: 'https://www.youtube.com/watch?v=U1iwe3L5pzc', + media: { + type: 'video', + link: 'https://www.youtube.com/embed/U1iwe3L5pzc', + }, + }, + { + title: 'Search all your services', + content: + 'All of Spotify’s services are automatically indexed in Backstage. So our engineers can stop playing detective — no more spamming Slack channels asking if anyone knows who owns a particular service and where you can find its API, only to discover that the owner went on sabbatical three months ago and you have to hunt them down on a mountain in Tibet where they’re on a 12-day silent meditation retreat. At Spotify, anyone can always find anyone else’s service, inspect its APIs, and contact its current owner — all with one search.', + actionItemLink: 'https://www.youtube.com/watch?v=vcDL9tOv7Eo', + media: { + type: 'video', + link: 'https://www.youtube.com/embed/vcDL9tOv7Eo', + }, + }, + { + title: 'Manage data pipelines', + content: + 'We manage a lot of data pipelines (also known as workflows) here at Spotify. So, of course, we made a great workflows plugin for our version of Backstage. All our workflow tools — including a scheduler, log inspector, data lineage graph, and configurable alerts — are integrated into one simple interface.', + actionItemLink: 'https://www.youtube.com/watch?v=rH46MLNZIPM', + media: { + type: 'video', + link: 'https://www.youtube.com/embed/rH46MLNZIPM', + }, + }, + ]; + //#endregion + + return ( + <Layout> + <div className={clsx(demosStyles.communityPage)}> + {demoItems.map((demoItem, index) => ( + <BannerSection + key={index} + greyBackground={index % 2 === 0} + className={demosStyles.banner} + > + <BannerSectionGrid> + <ContentBlock + title={<h1>{demoItem.title}</h1>} + actionButtons={[ + { + link: demoItem.actionItemLink, + label: 'WATCH NOW', + }, + ]} + > + {demoItem.content} + </ContentBlock> + + <div className={clsx()}> + {demoItem.media.type === 'image' ? ( + <img src={demoItem.media.link} alt={demoItem.title} /> + ) : ( + <iframe + src={demoItem.media.link} + title={demoItem.title} + allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" + allowFullScreen + ></iframe> + )} + </div> + </BannerSectionGrid> + </BannerSection> + ))} + </div> + </Layout> + ); +}; + +export default Demos; diff --git a/microsite-next/src/pages/home/_home.tsx b/microsite-next/src/pages/home/_home.tsx new file mode 100644 index 0000000000..4ba5539348 --- /dev/null +++ b/microsite-next/src/pages/home/_home.tsx @@ -0,0 +1,538 @@ +import BrowserOnly from '@docusaurus/BrowserOnly'; +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import { BannerSection } from '@site/src/components/bannerSection/bannerSection'; +import { BannerSectionGrid } from '@site/src/components/bannerSection/bannerSectionGrid'; +import { ContentBlock } from '@site/src/components/contentBlock/contentBlock'; +import Layout from '@theme/Layout'; +import { clsx } from 'clsx'; +import React, { useState } from 'react'; + +import homeStyles from './home.module.scss'; +import { HubSpotNewAdoptersForm } from './_hubSpotNewAdoptersForm'; + +const hiddenNewsletterBannerKey = 'hiddenNewsletterBanner'; + +const HomePage = () => { + const { siteConfig } = useDocusaurusContext(); + + const [hiddenNewsletterBanner, setHideNewsletterBanner] = useState(() => { + return JSON.parse(localStorage.getItem(hiddenNewsletterBannerKey)) || false; + }); + + const hideNewsletterBanner = (shouldHide: boolean) => { + localStorage.setItem(hiddenNewsletterBannerKey, JSON.stringify(shouldHide)); + setHideNewsletterBanner(shouldHide); + }; + + return ( + <Layout> + <div className={homeStyles.homePage}> + <BannerSection greyBackground> + <BannerSectionGrid + header={ + !hiddenNewsletterBanner && ( + <div + className={clsx( + 'card', + 'padding--md', + homeStyles.newsletterBanner, + )} + > + <div className="text--left bannerContent"> + 🗞️ Want to stay up to date with Backstage? Sign up for our{' '} + <Link + to="https://info.backstage.spotify.com/newsletter_subscribe" + className="text--secondary" + > + Newsletter + </Link> + ! + </div> + + <div + className={clsx('button button--link', 'bannerCloseButton')} + onClick={() => hideNewsletterBanner(true)} + > + <svg + className="text--secondary" + width="24" + height="24" + viewBox="0 0 24 24" + > + <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" /> + </svg> + </div> + </div> + ) + } + > + <ContentBlock + className={homeStyles.openPlatformBanner} + title={<h1> An open platform for building developer portals</h1>} + actionButtons={[ + { + link: 'https://github.com/backstage/backstage#getting-started', + label: 'GITHUB', + }, + { + link: 'https://info.backstage.spotify.com/office-hours', + label: 'OFFICE HOURS', + }, + ]} + > + Powered by a centralized software catalog, Backstage restores + order to your infrastructure and enables your product teams to + ship high-quality code quickly — without compromising autonomy. + </ContentBlock> + + <div className={homeStyles.svgContainer}> + <img + className="laptopSvg" + src={`${siteConfig.baseUrl}img/laptop.svg`} + /> + <img + className="laptopScreenGif" + src={`${siteConfig.baseUrl}animations/backstage-logos-hero-8.gif`} + /> + </div> + </BannerSectionGrid> + </BannerSection> + + <BannerSection> + <BannerSectionGrid> + <ContentBlock + title="The Speed Paradox" + topImgSrc={`${siteConfig.baseUrl}animations/backstage-speed-paradox-7.gif`} + > + At Spotify, we've always believed in the speed and ingenuity that + comes from having autonomous development teams. But as we learned + firsthand, the faster you grow, the more fragmented and complex + your software ecosystem becomes. And then everything slows down + again. + </ContentBlock> + + <ContentBlock + title="The Standards Paradox" + topImgSrc={`${siteConfig.baseUrl}animations/backstage-standards-paradox-4.gif`} + > + By centralizing services and standardizing your tooling, Backstage + streamlines your development environment from end to end. Instead + of restricting autonomy, standardization frees your engineers from + infrastructure complexity. So you can return to building and + scaling, quickly and safely. + </ContentBlock> + </BannerSectionGrid> + </BannerSection> + + <BannerSection greenGradientBackground> + <div className={homeStyles.catalogContainer}> + <div className="catalogTitle"> + <img + src={`${siteConfig.baseUrl}animations/backstage-software-catalog-icon-1.gif`} + alt="Software Catalog Planet GIF" + /> + + <h2 className="text--primary">Backstage Software Catalog</h2> + + <h1>Build an ecosystem, not a wilderness</h1> + </div> + + <picture className="catalogImg"> + <source + srcSet={`${siteConfig.baseUrl}img/components-with-filter.png`} + media="(min-width: 997px)" + /> + <img + src={`${siteConfig.baseUrl}img/components-with-filter-small.png`} + alt="Software Catalog Filter Sidebar" + /> + </picture> + + <ContentBlock + title="Manage all your software, all in one place" + hasBulletLine + > + Backstage makes it easy for one team to manage 10 services — and + makes it possible for your company to manage thousands of them + </ContentBlock> + + <ContentBlock title="A uniform overview" hasBulletLine> + Every team can see all the services they own and related resources + (deployments, data pipelines, pull request status, etc.) + </ContentBlock> + + <ContentBlock title="Metadata on tap" hasBulletLine> + All that information can be shared with plugins inside Backstage + to enable other management features, like resource monitoring and + testing + </ContentBlock> + + <ContentBlock title="Not just services" hasBulletLine> + Libraries, websites, ML models — you name it, Backstage knows all + about it, including who owns it, dependencies, and more + </ContentBlock> + + <ContentBlock + title="Discoverability & accountability" + hasBulletLine + > + No more orphan software hiding in the dark corners of your tech + stack + </ContentBlock> + </div> + </BannerSection> + + <BannerSection greenCallToActionGradientBackground> + <div className="padding--lg text--center"> + <h1>Learn more about the software catalog</h1> + + <Link + to="https://backstage.io/docs/features/software-catalog/software-catalog-overview" + className="button button--secondary" + > + READ + </Link> + </div> + </BannerSection> + + <BannerSection greenBottomGradientBackground> + <div className={homeStyles.softwareTemplatesContainer}> + <div className="softwareTemplatesTitle"> + <img + src={`${siteConfig.baseUrl}animations/backstage-software-templates-icon-5.gif`} + alt="Software Templates Rocket GIF" + /> + + <h2 className="text--primary">Backstage Software Templates</h2> + + <h1>Standards can set you free</h1> + </div> + + <picture className="softwareTemplatesImg"> + <source + srcSet={`${siteConfig.baseUrl}img/cards.png`} + media="(min-width: 997px)" + /> + <img + src={`${siteConfig.baseUrl}img/service-cards.png`} + alt="Software Templates Cards" + /> + </picture> + + <ContentBlock + title="Like automated getting started guides" + hasBulletLine + > + Using templates, engineers can spin up a new microservice with + your organization's best practices built-in, right from the start + </ContentBlock> + + <ContentBlock title="Push-button deployment" hasBulletLine> + Click a button to create a Spring Boot project with your repo + automatically configured on GitHub and your CI already running the + first build + </ContentBlock> + + <ContentBlock title="Built to your standards" hasBulletLine> + Go instead of Java? CircleCI instead of Jenkins? Serverless + instead of Kubernetes? GCP instead of AWS? Customize your recipes + with your best practices baked-in + </ContentBlock> + + <ContentBlock title="Golden Paths pave the way" hasBulletLine> + When the right way is also the easiest way, engineers get up and + running faster — and more safely + </ContentBlock> + </div> + </BannerSection> + + <BannerSection greenCallToActionGradientBackground> + <div className="padding--lg text--center"> + <h1>Build your own software templates</h1> + + <Link + to="https://backstage.io/docs/features/software-templates" + className="button button--secondary" + > + CONTRIBUTE + </Link> + </div> + </BannerSection> + + <BannerSection greenBottomGradientBackground> + <div className={homeStyles.softwareTemplatesContainer}> + <div className="softwareTemplatesTitle"> + <img + src={`${siteConfig.baseUrl}animations/backstage-techdocs-icon-1.gif`} + alt="Backstage TechDocs File Copy GIF" + /> + + <h2 className="text--primary">Backstage TechDocs</h2> + + <h1>Docs like code</h1> + </div> + + <picture className="softwareTemplatesImg"> + <source + srcSet={`${siteConfig.baseUrl}img/techdocs2.gif`} + media="(min-width: 997px)" + /> + <img + src={`${siteConfig.baseUrl}img/techdocs-static-mobile.png`} + alt="Backstage TechDocs Markdown to HTML" + /> + </picture> + + <ContentBlock title="Free documentation" hasBulletLine> + Whenever you use a Backstage Software Template, your project + automatically gets a TechDocs site, for free + </ContentBlock> + + <ContentBlock title="Easy to write" hasBulletLine> + With our docs-like-code approach, engineers write their + documentation in Markdown files right alongside their code + </ContentBlock> + + <ContentBlock title="Easy to maintain" hasBulletLine> + Updating code? Update your documentation while you're there — with + docs and code in the same place, it becomes a natural part of your + workstream + </ContentBlock> + + <ContentBlock title="Easy to find and use" hasBulletLine> + Since all your documentation is in Backstage, finding any TechDoc + is just a search query away + </ContentBlock> + </div> + </BannerSection> + + <BannerSection greenCallToActionGradientBackground> + <div className="padding--lg text--center"> + <h1>Learn more about TechDocs</h1> + + <Link + to="https://backstage.io/docs/features/techdocs/techdocs-overview" + className="button button--secondary" + > + DOCS + </Link> + </div> + </BannerSection> + + <BannerSection greenGradientBackground> + <div className={homeStyles.softwareTemplatesContainer}> + <div className="softwareTemplatesTitle"> + <img + src={`${siteConfig.baseUrl}animations/backstage-search-platform-icon-1.gif`} + alt="Search Platform Telescope GIF" + /> + + <h2 className="text--primary">Backstage Search Platform</h2> + + <h1>A search platform made just for you</h1> + </div> + + <picture className="softwareTemplatesImg"> + <source + srcSet={`${siteConfig.baseUrl}img/search-platform-overview.png`} + media="(min-width: 997px)" + /> + <img + src={`${siteConfig.baseUrl}img/search-platform-overview-small.png`} + alt="Search Platform Search Bar" + /> + </picture> + + <ContentBlock title="Way more than a text box" hasBulletLine> + Backstage Search more than just a box you type questions into — + it's an entire platform all by itself, which you can customize to + fit your organization's needs + </ContentBlock> + + <ContentBlock title="Search the way you want" hasBulletLine> + Bring your own search engine, create a customized search page + experience, or edit the look and feel of each search result + </ContentBlock> + + <ContentBlock title="Index everything, find anything" hasBulletLine> + With an extensible backend, you can search beyond the Software + Catalog and index any source you'd like — whether it's TechDocs or + Confluence and Stack Overflow + </ContentBlock> + + <ContentBlock title="Discoverability unlocked" hasBulletLine> + New hires and seasoned employees alike can easily search your + infrastructure instead of getting lost in it + </ContentBlock> + </div> + </BannerSection> + + <BannerSection greenCallToActionGradientBackground> + <div className="padding--lg text--center"> + <h1>Learn more about Backstage Search</h1> + + <Link + to="https://backstage.io/docs/features/search/search-overview" + className="button button--secondary" + > + READ + </Link> + </div> + </BannerSection> + + <BannerSection greenBottomGradientBackground> + <BannerSectionGrid + className={homeStyles.kubernetesSectionContainer} + header={ + <> + <img + src={`${siteConfig.baseUrl}animations/backstage-kubernetes-icon-1.gif`} + alt="Backstage Kubernetes Flag GIF" + /> + + <h2 className="text--primary">Backstage Kubernetes</h2> + + <h1>Manage your services, not clusters</h1> + </> + } + > + <ContentBlock + title="Kubernetes made just for service owners" + hasBulletLine + > + Backstage features the first Kubernetes monitoring tool designed + around the needs of service owners, not cluster admins + </ContentBlock> + + <ContentBlock title="Your service at a glance" hasBulletLine> + Get all your service's deployments in one, aggregated view — no + more digging through cluster logs in a CLI, no more combing + through lists of services you don't own + </ContentBlock> + + <ContentBlock title="Pick a cloud, any cloud" hasBulletLine> + Since Backstage uses the Kubernetes API, it's cloud agnostic — so + it works no matter which cloud provider or managed Kubernetes + service you use, and even works in multi-cloud orgs + </ContentBlock> + + <ContentBlock title="Any K8s, one UI" hasBulletLine> + Now you don't have to switch dashboards when you move from local + testing to production, or from one cloud provider to another + </ContentBlock> + </BannerSectionGrid> + </BannerSection> + + <BannerSection greenCallToActionGradientBackground> + <div className="padding--lg text--center"> + <h1>Learn more about the K8s plugin</h1> + + <Link + to="https://backstage.io/blog/2021/01/12/new-backstage-feature-kubernetes-for-service-owners" + className="button button--secondary" + > + READ + </Link> + </div> + </BannerSection> + + <BannerSection greenBottomGradientBackground> + <div className={homeStyles.softwareTemplatesContainer}> + <div className="softwareTemplatesTitle"> + <img + src={`${siteConfig.baseUrl}animations/backstage-plugin-icon-2.gif`} + alt="Plugins Building Blocks GIF" + /> + + <h2 className="text--primary"> + Customize Backstage with plugins + </h2> + + <h1>An app store for your infrastructure</h1> + </div> + + <picture className="softwareTemplatesImg"> + <source + srcSet={`${siteConfig.baseUrl}img/cards-plugins.png`} + media="(min-width: 997px)" + /> + <img + src={`${siteConfig.baseUrl}img/plugins.png`} + alt="Plugins Cards" + /> + </picture> + + <ContentBlock title="Add functionality" hasBulletLine> + Want scalable website testing? Add the{' '} + <Link to="https://backstage.io/blog/2020/04/06/lighthouse-plugin"> + Lighthouse{' '} + </Link> + plugin. Wondering about recommended frameworks? Add the{' '} + <Link to="https://backstage.io/blog/2020/05/14/tech-radar-plugin"> + Tech Radar{' '} + </Link> + plugin. + </ContentBlock> + + <ContentBlock title="BYO Plugins" hasBulletLine> + If you don't see the plugin you need, it's simple to build your + own + </ContentBlock> + + <ContentBlock + title="Integrate your own custom tooling" + hasBulletLine + > + Building internal plugins lets you tailor your version of + Backstage to be a perfect fit for your infrastructure + </ContentBlock> + + <ContentBlock title="Share with the community" hasBulletLine> + Building <Link to="/plugins">open source plugins</Link>{' '} + contributes to the entire Backstage ecosystem, which benefits + everyone + </ContentBlock> + </div> + </BannerSection> + + <BannerSection greenCallToActionGradientBackground> + <div className="padding--lg text--center"> + <h1>Build a plugin</h1> + + <Link + to="/docs/plugins/create-a-plugin" + className="button button--secondary" + > + CONTRIBUTE + </Link> + </div> + </BannerSection> + + <BannerSection> + <div className="padding--lg text--center"> + <h2> + Backstage is a{' '} + <Link to="https://www.cncf.io"> + Cloud Native Computing Foundation + </Link>{' '} + incubation project + </h2> + + <img + src={`${siteConfig.baseUrl}img/cncf-white.svg`} + alt="CNCF Logo" + height="100px" + /> + </div> + </BannerSection> + + <HubSpotNewAdoptersForm /> + </div> + </Layout> + ); +}; + +export const Home = () => { + return <BrowserOnly>{() => <HomePage />}</BrowserOnly>; +}; diff --git a/microsite-next/src/pages/home/_hubSpotNewAdoptersForm.tsx b/microsite-next/src/pages/home/_hubSpotNewAdoptersForm.tsx new file mode 100644 index 0000000000..36f00dff75 --- /dev/null +++ b/microsite-next/src/pages/home/_hubSpotNewAdoptersForm.tsx @@ -0,0 +1,52 @@ +import clsx from 'clsx'; +import React, { useEffect, useState } from 'react'; + +import hubSpotStyles from './hubSpotNewAdoptersForm.module.scss'; + +export const HubSpotNewAdoptersForm = () => { + const [isClosed, setClosed] = useState(true); + + useEffect(() => { + const script = document.createElement('script'); + script.src = 'https://js.hsforms.net/forms/v2.js'; + document.body.appendChild(script); + + script.addEventListener('load', () => { + // @ts-ignore + if (window.hbspt) { + // @ts-ignore + window.hbspt.forms.create({ + portalId: '21894833', + formId: '9a5aa2af-87f3-4a44-819f-88ee243bb61e', + target: `.${hubSpotStyles.hubSpotNewAdopterFormContent}`, + pageId: '79735607665', + }); + } + }); + }, []); + + return ( + <div + className={clsx( + hubSpotStyles.hubSpotNewAdopterFormContainer, + isClosed && 'adoptersFormHidden', + )} + > + <button + className="button button--primary" + onClick={() => { + setClosed(!isClosed); + }} + > + New Adopters + </button> + + <div + className={clsx( + 'padding-left--lg padding-vert--lg thin-scrollbar', + hubSpotStyles.hubSpotNewAdopterFormContent, + )} + ></div> + </div> + ); +}; diff --git a/microsite-next/src/pages/home/home.module.scss b/microsite-next/src/pages/home/home.module.scss new file mode 100644 index 0000000000..57f2b9acde --- /dev/null +++ b/microsite-next/src/pages/home/home.module.scss @@ -0,0 +1,109 @@ +$desktopUpBreakpoint: 997px; + +.homePage { + font-size: 1.25rem; + + h1 { + font-size: 54px; + line-height: 56px; + word-break: break-word; + } + + :global(.buttonsContainer) { + gap: 1rem; + display: grid; + grid-template-columns: repeat(2, auto); + } +} + +.newsletterBanner { + --ifm-link-hover-color: var(--ifm-color-info); + + flex-direction: row; + color: var(--ifm-button-color); + background-color: var(--ifm-color-primary); + + :global(.bannerContent) { + flex: 1; + } + + :global(.bannerCloseButton) { + color: var(--ifm-color-secondary); + } +} + +.svgContainer { + position: relative; + + :global(img.laptopSvg) { + width: 100%; + } + + :global(img.laptopScreenGif) { + position: absolute; + top: 3%; + left: 17%; + width: 70%; + } +} + +// Shared container styles +.catalogContainer, +.softwareTemplatesContainer { + gap: 2rem; + display: grid; + align-items: center; + + picture { + display: block; + text-align: center; + } + + :global([class*='Title']), + :global([class*='title']), + :global([class*='gridHeader']) { + img { + max-width: 190px; + max-height: 190px; + } + } +} + +.catalogContainer { + @media (min-width: $desktopUpBreakpoint) { + grid-template-columns: repeat(4, 1fr); + + picture { + grid-row: span 2; + grid-column: span 2; + } + + :global(.catalogTitle) { + grid-column: span 2; + } + } +} + +.softwareTemplatesContainer { + @media (min-width: $desktopUpBreakpoint) { + grid: repeat(2, auto) / repeat(4, 1fr); + + picture { + grid-row-start: 3; + grid-column: span 4; + } + + :global(.softwareTemplatesTitle) { + grid-column: span 4; + } + } +} + +.kubernetesSectionContainer { + :global([class*='gridHeader']) { + img { + max-width: 190px; + max-height: 190px; + } + } +} diff --git a/microsite-next/src/pages/home/hubSpotNewAdoptersForm.module.scss b/microsite-next/src/pages/home/hubSpotNewAdoptersForm.module.scss new file mode 100644 index 0000000000..77e1a87a53 --- /dev/null +++ b/microsite-next/src/pages/home/hubSpotNewAdoptersForm.module.scss @@ -0,0 +1,38 @@ +.hubSpotNewAdopterFormContainer { + top: 50%; + right: 0; + display: flex; + position: fixed; + transform: translateY(-50%); + + transition: all 250ms ease-in-out; + + &:global(.adoptersFormHidden) { + transform: translate(500px, -50%); + } + + > button { + top: 50%; + left: -2rem; + position: fixed; + + transform: rotate(-90deg) translateX(-50%); + transform-origin: top left; + } +} + +.hubSpotNewAdopterFormContent { + height: 600px; + max-width: 500px; + overflow-y: hidden; + border-radius: 8px 0 0 8px; + background-color: var(--ifm-color-white); + + form { + width: 100%; + height: 100%; + overflow-y: auto; + + padding-right: 2rem; + } +} diff --git a/microsite-next/src/pages/index.tsx b/microsite-next/src/pages/index.tsx index 3bbf03b0e2..853a4b2c82 100644 --- a/microsite-next/src/pages/index.tsx +++ b/microsite-next/src/pages/index.tsx @@ -1,11 +1,3 @@ -import Layout from '@theme/Layout'; -import React from 'react'; +import { Home } from './home/_home'; -function Index() { - return ( - <Layout> - <h1>HELLO</h1> - </Layout> - ); -} -export default Index; +export default Home; diff --git a/microsite-next/src/pages/plugins/_pluginCard.tsx b/microsite-next/src/pages/plugins/_pluginCard.tsx new file mode 100644 index 0000000000..dec62af06f --- /dev/null +++ b/microsite-next/src/pages/plugins/_pluginCard.tsx @@ -0,0 +1,54 @@ +import Link from '@docusaurus/Link'; +import React from 'react'; + +export interface IPluginData { + author: string; + authorUrl: string; + category: string; + description: string; + documentation: string; + iconUrl: string; + title: string; + order?: number; +} + +const defaultIconUrl = 'img/logo-gradient-on-dark.svg'; + +export const PluginCard = ({ + author, + authorUrl, + category, + description, + documentation, + iconUrl, + title, +}: IPluginData) => ( + <div className="card"> + <div className="card__header"> + <img src={iconUrl || defaultIconUrl} alt={title} /> + + <h3>{title}</h3> + + <p className="PluginCardAuthor"> + by <a href={authorUrl}>{author}</a> + </p> + + <span className="button button--sm button--outline button--primary"> + {category} + </span> + </div> + + <div className="card__body"> + <p>{description}</p> + </div> + + <div className="card__footer"> + <Link + to={documentation} + className="button button--outline button--primary button--block" + > + Explore + </Link> + </div> + </div> +); diff --git a/microsite-next/src/pages/plugins/index.tsx b/microsite-next/src/pages/plugins/index.tsx new file mode 100644 index 0000000000..f8c6f78819 --- /dev/null +++ b/microsite-next/src/pages/plugins/index.tsx @@ -0,0 +1,87 @@ +import Link from '@docusaurus/Link'; +import Layout from '@theme/Layout'; +import clsx from 'clsx'; +import React from 'react'; + +import { IPluginData, PluginCard } from './_pluginCard'; +import pluginsStyles from './plugins.module.scss'; + +//#region Plugin data import +const maxDescLength = 160; + +const truncatePluginDescription = (pluginData: IPluginData) => + pluginData.description.length > maxDescLength + ? { + ...pluginData, + description: pluginData.description.slice(0, maxDescLength) + '...', + } + : pluginData; + +const pluginsContext = require.context( + '../../../../microsite/data/plugins', + false, + /\.ya?ml/, +); + +const plugins = pluginsContext.keys().reduce( + (acum, id) => { + const pluginData: IPluginData = pluginsContext(id).default; + + acum[ + pluginData.category === 'Core Feature' ? 'corePlugins' : 'otherPlugins' + ].push(truncatePluginDescription(pluginData)); + + return acum; + }, + { corePlugins: [] as IPluginData[], otherPlugins: [] as IPluginData[] }, +); + +plugins.corePlugins.sort((a, b) => a.order - b.order); +plugins.otherPlugins.sort((a, b) => a.order - b.order); +//#endregion + +const Plugins = () => ( + <Layout> + <div + className={clsx('container', 'padding--lg', pluginsStyles.pluginsPage)} + > + <div className="marketplaceBanner"> + <div className="marketplaceContent"> + <h2>Plugin Marketplace</h2> + + <p> + Open source plugins that you can add to your Backstage deployment. + Learn how to build a <Link to="/docs/plugins">plugin</Link>. + </p> + </div> + + <Link + to="/docs/plugins/add-to-marketplace" + className="button button--outline button--primary" + > + Add to Marketplace + </Link> + </div> + + <div className="bulletLine margin-bottom--lg"></div> + + <h2>Core Features</h2> + + <div className="pluginsContainer margin-bottom--lg"> + {plugins.corePlugins.map(pluginData => ( + <PluginCard key={pluginData.title} {...pluginData}></PluginCard> + ))} + </div> + + <h2>All Plugins</h2> + + <div className="pluginsContainer margin-bottom--lg"> + {plugins.otherPlugins.map(pluginData => ( + <PluginCard key={pluginData.title} {...pluginData}></PluginCard> + ))} + </div> + </div> + </Layout> +); + +export default Plugins; diff --git a/microsite-next/src/pages/plugins/plugins.module.scss b/microsite-next/src/pages/plugins/plugins.module.scss new file mode 100644 index 0000000000..d21444583a --- /dev/null +++ b/microsite-next/src/pages/plugins/plugins.module.scss @@ -0,0 +1,33 @@ +.pluginsPage { + :global(.marketplaceBanner) { + display: flex; + align-items: center; + } + + :global(.marketplaceContent) { + flex: 1; + } + + :global(.card .card__header) { + display: grid; + row-gap: 0.25rem; + column-gap: 1rem; + align-items: center; + grid-template-columns: auto 1fr; + + > * { + margin: 0; + } + + :global(img) { + width: 80px; + grid-row: 1/4; + } + } + + :global(.pluginsContainer) { + gap: 1rem; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + } +} diff --git a/microsite-next/tsconfig.json b/microsite-next/tsconfig.json new file mode 100644 index 0000000000..ae8fe2298a --- /dev/null +++ b/microsite-next/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/docusaurus/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "types": [ + "node", + "@docusaurus/module-type-aliases", + "@docusaurus/theme-classic", + "@types/webpack-env" + ] + } +} diff --git a/microsite-next/typings.d.ts b/microsite-next/typings.d.ts new file mode 100644 index 0000000000..d5cf927a7c --- /dev/null +++ b/microsite-next/typings.d.ts @@ -0,0 +1 @@ +declare module '*.scss'; diff --git a/microsite-next/yarn.lock b/microsite-next/yarn.lock index 3a632b3079..517e3c2280 100644 --- a/microsite-next/yarn.lock +++ b/microsite-next/yarn.lock @@ -1660,9 +1660,9 @@ __metadata: languageName: node linkType: hard -"@docusaurus/core@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/core@npm:2.2.0" +"@docusaurus/core@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/core@npm:2.3.1" dependencies: "@babel/core": ^7.18.6 "@babel/generator": ^7.18.7 @@ -1674,13 +1674,13 @@ __metadata: "@babel/runtime": ^7.18.6 "@babel/runtime-corejs3": ^7.18.6 "@babel/traverse": ^7.18.8 - "@docusaurus/cssnano-preset": 2.2.0 - "@docusaurus/logger": 2.2.0 - "@docusaurus/mdx-loader": 2.2.0 + "@docusaurus/cssnano-preset": 2.3.1 + "@docusaurus/logger": 2.3.1 + "@docusaurus/mdx-loader": 2.3.1 "@docusaurus/react-loadable": 5.5.2 - "@docusaurus/utils": 2.2.0 - "@docusaurus/utils-common": 2.2.0 - "@docusaurus/utils-validation": 2.2.0 + "@docusaurus/utils": 2.3.1 + "@docusaurus/utils-common": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 "@slorber/static-site-generator-webpack-plugin": ^4.0.7 "@svgr/webpack": ^6.2.1 autoprefixer: ^10.4.7 @@ -1701,7 +1701,7 @@ __metadata: del: ^6.1.1 detect-port: ^1.3.0 escape-html: ^1.0.3 - eta: ^1.12.3 + eta: ^2.0.0 file-loader: ^6.2.0 fs-extra: ^10.1.0 html-minifier-terser: ^6.1.0 @@ -1740,40 +1740,40 @@ __metadata: react-dom: ^16.8.4 || ^17.0.0 bin: docusaurus: bin/docusaurus.mjs - checksum: ff47e6cf85b0f7dc0a9e5b9b0d26e33a6f7385f067566ff4f9b026d044839e4dfb4c3bc9476cfab7a7e95a0065478a534cda403dac3bb7bac9987406f1978a11 + checksum: 812aecae45af3f4d02fd16e89517ca9f1ba22821a078aaa890f5797ac7e0cc0c79e7623eb999e885cf7e7652a6ffda8ff7c06dfd85ca29aaab600993c3d9980d languageName: node linkType: hard -"@docusaurus/cssnano-preset@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/cssnano-preset@npm:2.2.0" +"@docusaurus/cssnano-preset@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/cssnano-preset@npm:2.3.1" dependencies: cssnano-preset-advanced: ^5.3.8 postcss: ^8.4.14 postcss-sort-media-queries: ^4.2.1 tslib: ^2.4.0 - checksum: eff9707414867bf844ef5d84bde1c843593b9b7f542dd1a0a7acc88798b0c5ddb721124229912c234bd88b93cb18d8d69c6115cbf706c2a790497f7d9dd23757 + checksum: a3d00ce86b16caffde36734bb2f4541d2c0df5e8ab6891a78ad05bccc631a895fecb04c385626ebcb8f905510c28fa6158288585673ae96565532d4ee4b60d4f languageName: node linkType: hard -"@docusaurus/logger@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/logger@npm:2.2.0" +"@docusaurus/logger@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/logger@npm:2.3.1" dependencies: chalk: ^4.1.2 tslib: ^2.4.0 - checksum: b3ce6e18721a34793a892221485c941d5f7112ae96d569f7918d12c1f50bde9c99bc4195f4d225e874b2bd5800a35413bfeaf78b63c6fbae5f3015d44d118eee + checksum: eff5f258aeac9c643431426256e3bc4515074cc3cc754fa643579ba427ba232ecace9a9579ae5af542330b22d7361892a1eaf84526983a0c821c5ca3ee895176 languageName: node linkType: hard -"@docusaurus/mdx-loader@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/mdx-loader@npm:2.2.0" +"@docusaurus/mdx-loader@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/mdx-loader@npm:2.3.1" dependencies: "@babel/parser": ^7.18.8 "@babel/traverse": ^7.18.8 - "@docusaurus/logger": 2.2.0 - "@docusaurus/utils": 2.2.0 + "@docusaurus/logger": 2.3.1 + "@docusaurus/utils": 2.3.1 "@mdx-js/mdx": ^1.6.22 escape-html: ^1.0.3 file-loader: ^6.2.0 @@ -1790,16 +1790,16 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: fee586498f43c46581062e681424c4637e75d505d813d8bf25f5315c912560f6600cd925bc5b07a93d5d5966741439578e7e72f30030b4c58a5cfdf72e0d8928 + checksum: 4a1c9ef0e8506ab4d9cb4714ff7437664e238e0f2878a5eb4a2e082897bbee7ae8d0b61ba9d45ffa820beb5ce75aa0050201db815b00c18fc136aaa4c6411c21 languageName: node linkType: hard -"@docusaurus/module-type-aliases@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/module-type-aliases@npm:2.2.0" +"@docusaurus/module-type-aliases@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/module-type-aliases@npm:2.3.1" dependencies: "@docusaurus/react-loadable": 5.5.2 - "@docusaurus/types": 2.2.0 + "@docusaurus/types": 2.3.1 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" @@ -1809,21 +1809,41 @@ __metadata: peerDependencies: react: "*" react-dom: "*" - checksum: ebcb9dff2f88b5962cd34aaa78b1a48531da4776229ef507665e3f053cccb185aadcc16c3703f21031e14ccb6c8312662a6eec1a2a06bc0a423221ad200e1e9e + checksum: 74f799f81455dc8ff3e6edf07428996764014c2c7b416e6b5d160af15f00ad3aa1ab75dee5356645ec7f2ea832fb2aca6e9a32b19d64abeb9e3d57c4195f1e50 languageName: node linkType: hard -"@docusaurus/plugin-content-blog@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/plugin-content-blog@npm:2.2.0" +"@docusaurus/plugin-client-redirects@npm:^2.3.1": + version: 2.3.1 + resolution: "@docusaurus/plugin-client-redirects@npm:2.3.1" dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/logger": 2.2.0 - "@docusaurus/mdx-loader": 2.2.0 - "@docusaurus/types": 2.2.0 - "@docusaurus/utils": 2.2.0 - "@docusaurus/utils-common": 2.2.0 - "@docusaurus/utils-validation": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/logger": 2.3.1 + "@docusaurus/utils": 2.3.1 + "@docusaurus/utils-common": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 + eta: ^2.0.0 + fs-extra: ^10.1.0 + lodash: ^4.17.21 + tslib: ^2.4.0 + peerDependencies: + react: ^16.8.4 || ^17.0.0 + react-dom: ^16.8.4 || ^17.0.0 + checksum: bb730359c7f9015108a3d4ec8a570e9c73847349747bf71711f522a7dc40007786b7cd25cebc845eeb16b3505e8068242559a2eda790f4c07d4083d9014a6f84 + languageName: node + linkType: hard + +"@docusaurus/plugin-content-blog@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/plugin-content-blog@npm:2.3.1" + dependencies: + "@docusaurus/core": 2.3.1 + "@docusaurus/logger": 2.3.1 + "@docusaurus/mdx-loader": 2.3.1 + "@docusaurus/types": 2.3.1 + "@docusaurus/utils": 2.3.1 + "@docusaurus/utils-common": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 cheerio: ^1.0.0-rc.12 feed: ^4.2.2 fs-extra: ^10.1.0 @@ -1836,21 +1856,21 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 6d51e3b17b6fdeb4e04ddebe4d4ba8c7cc830bdc066c2b7898e4dee185e408f0d28ea873d18b5ee4406a568a9b05f70d17c986a9ed16b16b1450d34ca190fd06 + checksum: abc668ceab15269f57be7f74acbec2e139b4f6b90af8771d246a9036d124b49b0d9fd4890e9566df7a4ba960f2da0316c18741eed1be0646f2b4602465219ddd languageName: node linkType: hard -"@docusaurus/plugin-content-docs@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/plugin-content-docs@npm:2.2.0" +"@docusaurus/plugin-content-docs@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/plugin-content-docs@npm:2.3.1" dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/logger": 2.2.0 - "@docusaurus/mdx-loader": 2.2.0 - "@docusaurus/module-type-aliases": 2.2.0 - "@docusaurus/types": 2.2.0 - "@docusaurus/utils": 2.2.0 - "@docusaurus/utils-validation": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/logger": 2.3.1 + "@docusaurus/mdx-loader": 2.3.1 + "@docusaurus/module-type-aliases": 2.3.1 + "@docusaurus/types": 2.3.1 + "@docusaurus/utils": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 "@types/react-router-config": ^5.0.6 combine-promises: ^1.1.0 fs-extra: ^10.1.0 @@ -1863,116 +1883,132 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 3a262b49dd6f9d59f4e10dd25185bb4280dbf77b62e28a1dd658d5db0861ae8c82dd025f24212f0d8fec0a46a37f6ef0f2cde25ac736d445247e8727177da660 + checksum: bcf8d921407d11b497926a1f61a1dc8c96f82fbe5a1959cc106b082e555f8fb6f42cf9262a658acf33d9543e5eb3e778049d91f71e4a2855993dc759c845cf31 languageName: node linkType: hard -"@docusaurus/plugin-content-pages@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/plugin-content-pages@npm:2.2.0" +"@docusaurus/plugin-content-pages@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/plugin-content-pages@npm:2.3.1" dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/mdx-loader": 2.2.0 - "@docusaurus/types": 2.2.0 - "@docusaurus/utils": 2.2.0 - "@docusaurus/utils-validation": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/mdx-loader": 2.3.1 + "@docusaurus/types": 2.3.1 + "@docusaurus/utils": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 fs-extra: ^10.1.0 tslib: ^2.4.0 webpack: ^5.73.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 1e22fb8deb9b8f612ebe1ea6f8b1ce76acfc6eb8cbc0d5fc9b99b99d64e2f356d0fb136247e9f72cd84b2788eaf953a640d23ff7e2a5d650de6ec06468181a94 + checksum: 8543050ed7330f54a28c0daeef11662eed3f3a08a6d0015b1a32db3d5e9ec46f0c6a8a5a4cb3e871ce953074d60424cc418b7ffa280695294626855a7a1a146a languageName: node linkType: hard -"@docusaurus/plugin-debug@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/plugin-debug@npm:2.2.0" +"@docusaurus/plugin-debug@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/plugin-debug@npm:2.3.1" dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/types": 2.2.0 - "@docusaurus/utils": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/types": 2.3.1 + "@docusaurus/utils": 2.3.1 fs-extra: ^10.1.0 react-json-view: ^1.21.3 tslib: ^2.4.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: edf2a416b790591c66ffa8ca1fd4ed15ab2d2dc15cd67c5253714502a6828739a7a47996c3664731c6b24da1da5862ddfef60defb84bd3b8273313267db0cb54 + checksum: e660575f900eedbeab6e222eb4f8ef6a7a49815c91a97839a4839737c0b3101698bf7c6e035cbafaa49010340010a9ec0d37270dc81a470b3bae42662c7a24b8 languageName: node linkType: hard -"@docusaurus/plugin-google-analytics@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/plugin-google-analytics@npm:2.2.0" +"@docusaurus/plugin-google-analytics@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/plugin-google-analytics@npm:2.3.1" dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/types": 2.2.0 - "@docusaurus/utils-validation": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/types": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 tslib: ^2.4.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 44ad3a6c1b661516cb87553103565af64a6f145d823b16882d5c7d23b99e091b7c4ba8323c5f6fe756e70fbb0f9f31d56c74512dc17da6d3c16dfabd17d719ac + checksum: 9306ae89cd5fb8ca86fd58809d7e624f988411d8908a151e9b6d9e8d0b84e08f1e3eba46024bc4321bcaeb3e9bc38e919b0bcf561adc9d40fa97c8ffeb232888 languageName: node linkType: hard -"@docusaurus/plugin-google-gtag@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/plugin-google-gtag@npm:2.2.0" +"@docusaurus/plugin-google-gtag@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/plugin-google-gtag@npm:2.3.1" dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/types": 2.2.0 - "@docusaurus/utils-validation": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/types": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 tslib: ^2.4.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 4e7d6fcc3f30f1d54933fdeb59d3065989596e91940304965635867808d89c7b864a394f5fab2bcde98037539bf6840efc692e856fb7a4ae32ce8b5f8a4e191a + checksum: 494f0405d79aa9cb36d1ea4cf739499ad15b59fe876573ab5b304b5e84ab6ef4d428ebdc26647777b0816af452f62959b5ddb25e5bbf73c7fb3d6568258980d0 languageName: node linkType: hard -"@docusaurus/plugin-sitemap@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/plugin-sitemap@npm:2.2.0" +"@docusaurus/plugin-google-tag-manager@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/plugin-google-tag-manager@npm:2.3.1" dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/logger": 2.2.0 - "@docusaurus/types": 2.2.0 - "@docusaurus/utils": 2.2.0 - "@docusaurus/utils-common": 2.2.0 - "@docusaurus/utils-validation": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/types": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 + tslib: ^2.4.0 + peerDependencies: + react: ^16.8.4 || ^17.0.0 + react-dom: ^16.8.4 || ^17.0.0 + checksum: d0b2ccc212652bb4f1c1bd61420e7f235325d4f2e8de0f5b25282305f54209d05da981c1253325bcae9afbc7575bd5c246d037d2be5fbda06f2842ba8335ab47 + languageName: node + linkType: hard + +"@docusaurus/plugin-sitemap@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/plugin-sitemap@npm:2.3.1" + dependencies: + "@docusaurus/core": 2.3.1 + "@docusaurus/logger": 2.3.1 + "@docusaurus/types": 2.3.1 + "@docusaurus/utils": 2.3.1 + "@docusaurus/utils-common": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 fs-extra: ^10.1.0 sitemap: ^7.1.1 tslib: ^2.4.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 8ae78093d17a96fc2c6f3829d425731dae3af19b0eec29c61a6465342462a8c24da4c5a10f1a1b1813630d2408f2e11fa17af652b74b4e8fda975d4a00bf1389 + checksum: 667d2abbf46efffc4d20e38fe435a19392f07726446193a017306652ee9db3d478e971eefb209e1a5c243b6b82af3de72d4b975b8e74aa93bda4711ce8c309bc languageName: node linkType: hard -"@docusaurus/preset-classic@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/preset-classic@npm:2.2.0" +"@docusaurus/preset-classic@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/preset-classic@npm:2.3.1" dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/plugin-content-blog": 2.2.0 - "@docusaurus/plugin-content-docs": 2.2.0 - "@docusaurus/plugin-content-pages": 2.2.0 - "@docusaurus/plugin-debug": 2.2.0 - "@docusaurus/plugin-google-analytics": 2.2.0 - "@docusaurus/plugin-google-gtag": 2.2.0 - "@docusaurus/plugin-sitemap": 2.2.0 - "@docusaurus/theme-classic": 2.2.0 - "@docusaurus/theme-common": 2.2.0 - "@docusaurus/theme-search-algolia": 2.2.0 - "@docusaurus/types": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/plugin-content-blog": 2.3.1 + "@docusaurus/plugin-content-docs": 2.3.1 + "@docusaurus/plugin-content-pages": 2.3.1 + "@docusaurus/plugin-debug": 2.3.1 + "@docusaurus/plugin-google-analytics": 2.3.1 + "@docusaurus/plugin-google-gtag": 2.3.1 + "@docusaurus/plugin-google-tag-manager": 2.3.1 + "@docusaurus/plugin-sitemap": 2.3.1 + "@docusaurus/theme-classic": 2.3.1 + "@docusaurus/theme-common": 2.3.1 + "@docusaurus/theme-search-algolia": 2.3.1 + "@docusaurus/types": 2.3.1 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 70214f17766097a2e9c4b21a343bf323f7ed3d2e23c6169577cd14333a074fa15aabff6532c1774ec17c54f50c1616dbd8625c41a115d2fe799b2b7fa830c2c9 + checksum: e4128a1bcb64d5ced04a281476ec1ae2d5523040d41ed57a8f744fb83659a2a2be902e94989de69ab1e6d693ec26c60d1ef6b2fe3ec96d5af6c9b3ef58f5b0cd languageName: node linkType: hard @@ -1988,22 +2024,22 @@ __metadata: languageName: node linkType: hard -"@docusaurus/theme-classic@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/theme-classic@npm:2.2.0" +"@docusaurus/theme-classic@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/theme-classic@npm:2.3.1" dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/mdx-loader": 2.2.0 - "@docusaurus/module-type-aliases": 2.2.0 - "@docusaurus/plugin-content-blog": 2.2.0 - "@docusaurus/plugin-content-docs": 2.2.0 - "@docusaurus/plugin-content-pages": 2.2.0 - "@docusaurus/theme-common": 2.2.0 - "@docusaurus/theme-translations": 2.2.0 - "@docusaurus/types": 2.2.0 - "@docusaurus/utils": 2.2.0 - "@docusaurus/utils-common": 2.2.0 - "@docusaurus/utils-validation": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/mdx-loader": 2.3.1 + "@docusaurus/module-type-aliases": 2.3.1 + "@docusaurus/plugin-content-blog": 2.3.1 + "@docusaurus/plugin-content-docs": 2.3.1 + "@docusaurus/plugin-content-pages": 2.3.1 + "@docusaurus/theme-common": 2.3.1 + "@docusaurus/theme-translations": 2.3.1 + "@docusaurus/types": 2.3.1 + "@docusaurus/utils": 2.3.1 + "@docusaurus/utils-common": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 "@mdx-js/react": ^1.6.22 clsx: ^1.2.1 copy-text-to-clipboard: ^3.0.1 @@ -2020,20 +2056,20 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: ccfb0bef12178d0fbe3329a3238cd6bf7223ee03d890594676c06490eabfd59908bb1872c1a007f605db4edf402bc49cdf14aa7116550e95844d5135a92c2969 + checksum: 273812924fc29b2316aff554ae0302509ebeaca5aa829b58253e74d22a66e69444f1c324a2d5e8e170e6c6f27dd0d6927e6c6a22a7e0c14567ff777d04a5b0c1 languageName: node linkType: hard -"@docusaurus/theme-common@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/theme-common@npm:2.2.0" +"@docusaurus/theme-common@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/theme-common@npm:2.3.1" dependencies: - "@docusaurus/mdx-loader": 2.2.0 - "@docusaurus/module-type-aliases": 2.2.0 - "@docusaurus/plugin-content-blog": 2.2.0 - "@docusaurus/plugin-content-docs": 2.2.0 - "@docusaurus/plugin-content-pages": 2.2.0 - "@docusaurus/utils": 2.2.0 + "@docusaurus/mdx-loader": 2.3.1 + "@docusaurus/module-type-aliases": 2.3.1 + "@docusaurus/plugin-content-blog": 2.3.1 + "@docusaurus/plugin-content-docs": 2.3.1 + "@docusaurus/plugin-content-pages": 2.3.1 + "@docusaurus/utils": 2.3.1 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" @@ -2041,30 +2077,31 @@ __metadata: parse-numeric-range: ^1.3.0 prism-react-renderer: ^1.3.5 tslib: ^2.4.0 + use-sync-external-store: ^1.2.0 utility-types: ^3.10.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 23cbba8e7e24494c6d106ce3d0b90ef461580bfacef9f27dfbc4f0b33fcb349394faf2bedf0a44db8c455535e50e828e82270c8f159c3d8d60f0e0980170be4e + checksum: 6b902e9e782721c3c49bcdee5d969ea1c1138ebcb03891e34f827b16f2c06f43a86d95f240a60ed084539e9b16435312a41be7bff4e724f4fb209998dd4d3a59 languageName: node linkType: hard -"@docusaurus/theme-search-algolia@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/theme-search-algolia@npm:2.2.0" +"@docusaurus/theme-search-algolia@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/theme-search-algolia@npm:2.3.1" dependencies: "@docsearch/react": ^3.1.1 - "@docusaurus/core": 2.2.0 - "@docusaurus/logger": 2.2.0 - "@docusaurus/plugin-content-docs": 2.2.0 - "@docusaurus/theme-common": 2.2.0 - "@docusaurus/theme-translations": 2.2.0 - "@docusaurus/utils": 2.2.0 - "@docusaurus/utils-validation": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/logger": 2.3.1 + "@docusaurus/plugin-content-docs": 2.3.1 + "@docusaurus/theme-common": 2.3.1 + "@docusaurus/theme-translations": 2.3.1 + "@docusaurus/utils": 2.3.1 + "@docusaurus/utils-validation": 2.3.1 algoliasearch: ^4.13.1 algoliasearch-helper: ^3.10.0 clsx: ^1.2.1 - eta: ^1.12.3 + eta: ^2.0.0 fs-extra: ^10.1.0 lodash: ^4.17.21 tslib: ^2.4.0 @@ -2072,23 +2109,23 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 42b6cb0322d6c772b7796ea6e9693d596554ebd087792ad71238cebedf3b632bfa8005138d521bce1ff118f49aea7d72e5dc97a03f236b4728a2dc7576870071 + checksum: 965303068e43b11f58d20b95bb6dfc01d5e575c2070d2730b94303bd2a1d33794075cae43bfe472f08061bd8770f14c8eb54932274e6b39f954ab34e7cfc5689 languageName: node linkType: hard -"@docusaurus/theme-translations@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/theme-translations@npm:2.2.0" +"@docusaurus/theme-translations@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/theme-translations@npm:2.3.1" dependencies: fs-extra: ^10.1.0 tslib: ^2.4.0 - checksum: 7fe7d104fd094f2af2321986a86edef1eb8ab25415ea94ab1b242d08aec7627b3d5790001631621cd80c57c710714308aad5adfbf570cb74e0f01fda93b610be + checksum: dd3796be63c4c946af789c3da18ed2704a2fa90d8e752ba2b780a124dc13369ba590218afad0ac4ea2342f7331ccb9eb1be086226c950b8384978d94a15c57ad languageName: node linkType: hard -"@docusaurus/types@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/types@npm:2.2.0" +"@docusaurus/types@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/types@npm:2.3.1" dependencies: "@types/history": ^4.7.11 "@types/react": "*" @@ -2101,13 +2138,13 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 5166ca49bb9333e4d733e4bf8d49d65e11ea6b39e4d8eecc24e1de24d61d2459c52dd8bd27362b66b03e41df96acf1a449145211b3bf0c5a59a987c77102e8f1 + checksum: 91e52f37b97964112aa0d50ee4a6f534d7da941443af5ddc96418817c6ce532a98c73e67045ac703b582c7ed703ebb360205eec30da7f738c0105f2b3ae1a246 languageName: node linkType: hard -"@docusaurus/utils-common@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/utils-common@npm:2.2.0" +"@docusaurus/utils-common@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/utils-common@npm:2.3.1" dependencies: tslib: ^2.4.0 peerDependencies: @@ -2115,29 +2152,30 @@ __metadata: peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: 05d23a2f82a1bc119e3ad6b37481c9bc984f62efd3a79046567216784b78fb20fe7452252d610bb4c063e4ded8a7ab7efa1dc9f9f228357c20b9f4729c7a0576 + checksum: 405dc5b8aba9a97b2670ba8ff3911bbdaed274edc15214ab482a7159a07ad1c9b3198835a7bee42de4e0320d42bd402ed89ae6896744a364d64d89d9f78bcfb0 languageName: node linkType: hard -"@docusaurus/utils-validation@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/utils-validation@npm:2.2.0" +"@docusaurus/utils-validation@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/utils-validation@npm:2.3.1" dependencies: - "@docusaurus/logger": 2.2.0 - "@docusaurus/utils": 2.2.0 + "@docusaurus/logger": 2.3.1 + "@docusaurus/utils": 2.3.1 joi: ^17.6.0 js-yaml: ^4.1.0 tslib: ^2.4.0 - checksum: a30e47cf84628950176cc02a121f31b200b46cdccf02e80d76f24b51b9d33fccee35c43047f507b8fb48deb38f863580ecbcdc1393718c6f3a14fcd40d5d1ab6 + checksum: 1e5529d1d0c4fcd9006adf2e5b545458a7dba3877563fb444dcec472f27a3d8492d4c6fb5dd1071bb6e668a13a845d74b8f6c4b6387babfa0e467a9b8b237fda languageName: node linkType: hard -"@docusaurus/utils@npm:2.2.0": - version: 2.2.0 - resolution: "@docusaurus/utils@npm:2.2.0" +"@docusaurus/utils@npm:2.3.1": + version: 2.3.1 + resolution: "@docusaurus/utils@npm:2.3.1" dependencies: - "@docusaurus/logger": 2.2.0 + "@docusaurus/logger": 2.3.1 "@svgr/webpack": ^6.2.1 + escape-string-regexp: ^4.0.0 file-loader: ^6.2.0 fs-extra: ^10.1.0 github-slugger: ^1.4.0 @@ -2156,7 +2194,7 @@ __metadata: peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: d027a6d2417e043ac463402aadca22f1101f942daaf02330d9bb4743dcbe3bd2fd46d27dedf316fcf2b6698713fede974ba59eb5d4bc92c8959e23bc25e7a03a + checksum: e8bce9bbd98bf63664fcd7c0a5f8dec30dad31ed19e18d724b43189b04ecdc1174537e1d987293575ec18d421236fb92d3d39d28477e921507260a39c3f6d6d0 languageName: node linkType: hard @@ -2382,9 +2420,9 @@ __metadata: linkType: hard "@sideway/formula@npm:^3.0.0": - version: 3.0.0 - resolution: "@sideway/formula@npm:3.0.0" - checksum: 8ae26a0ed6bc84f7310be6aae6eb9d81e97f382619fc69025d346871a707eaab0fa38b8c857e3f0c35a19923de129f42d35c50b8010c928d64aab41578580ec4 + version: 3.0.1 + resolution: "@sideway/formula@npm:3.0.1" + checksum: e4beeebc9dbe2ff4ef0def15cec0165e00d1612e3d7cea0bc9ce5175c3263fc2c818b679bd558957f49400ee7be9d4e5ac90487e1625b4932e15c4aa7919c57a languageName: node linkType: hard @@ -2585,6 +2623,115 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-darwin-arm64@npm:1.3.36" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-darwin-x64@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-darwin-x64@npm:1.3.36" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@swc/core-linux-arm-gnueabihf@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.36" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@swc/core-linux-arm64-gnu@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.36" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-arm64-musl@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.36" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-linux-x64-gnu@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.36" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-musl@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-x64-musl@npm:1.3.36" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-win32-arm64-msvc@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.36" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-win32-ia32-msvc@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.36" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@swc/core-win32-x64-msvc@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.36" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:^1.3.36": + version: 1.3.36 + resolution: "@swc/core@npm:1.3.36" + dependencies: + "@swc/core-darwin-arm64": 1.3.36 + "@swc/core-darwin-x64": 1.3.36 + "@swc/core-linux-arm-gnueabihf": 1.3.36 + "@swc/core-linux-arm64-gnu": 1.3.36 + "@swc/core-linux-arm64-musl": 1.3.36 + "@swc/core-linux-x64-gnu": 1.3.36 + "@swc/core-linux-x64-musl": 1.3.36 + "@swc/core-win32-arm64-msvc": 1.3.36 + "@swc/core-win32-ia32-msvc": 1.3.36 + "@swc/core-win32-x64-msvc": 1.3.36 + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + checksum: 36e96e86d1ab8fd84eb51e7264a7ae781ee7e507d36b9ccc028d7f31a2ccf2949f074656e34139c970196774d24d55a54c3aa5c53d04eb6e3569c71cbfed0c0d + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^1.1.2": version: 1.1.2 resolution: "@szmarczak/http-timer@npm:1.1.2" @@ -2608,6 +2755,13 @@ __metadata: languageName: node linkType: hard +"@tsconfig/docusaurus@npm:^1.0.6": + version: 1.0.6 + resolution: "@tsconfig/docusaurus@npm:1.0.6" + checksum: fb0d7965c01fe64fc6369a72695b903d654bd5fb145f373d707c2bae3c2d828703517d812cef1c041ae44b108f44e52778b9d9837a54fdf782e68e6619a90a4d + languageName: node + linkType: hard + "@types/body-parser@npm:*": version: 1.19.2 resolution: "@types/body-parser@npm:1.19.2" @@ -2951,6 +3105,13 @@ __metadata: languageName: node linkType: hard +"@types/webpack-env@npm:^1.18.0": + version: 1.18.0 + resolution: "@types/webpack-env@npm:1.18.0" + checksum: ecf4daa31cb37d474ac0ce058d83a3cadeb9881ca8107ae93c2299eaa9954943aae09b43e143c62ccbe4288a14db00c918c9debd707afe17c3998f873eaabc59 + languageName: node + linkType: hard + "@types/ws@npm:^8.5.1": version: 8.5.3 resolution: "@types/ws@npm:8.5.3" @@ -3565,15 +3726,25 @@ __metadata: version: 0.0.0-use.local resolution: "backstage-microsite@workspace:." dependencies: - "@docusaurus/core": 2.2.0 - "@docusaurus/preset-classic": 2.2.0 + "@docusaurus/core": 2.3.1 + "@docusaurus/module-type-aliases": 2.3.1 + "@docusaurus/plugin-client-redirects": ^2.3.1 + "@docusaurus/preset-classic": 2.3.1 "@spotify/prettier-config": ^14.0.0 + "@swc/core": ^1.3.36 + "@tsconfig/docusaurus": ^1.0.6 + "@types/webpack-env": ^1.18.0 clsx: ^1.1.1 + docusaurus-plugin-sass: ^0.2.3 js-yaml: ^4.1.0 prettier: ^2.6.2 react: ^17.0.2 react-dom: ^17.0.2 replace: ^1.2.2 + sass: ^1.57.1 + swc-loader: ^0.2.3 + typescript: ^4.9.4 + yaml-loader: ^0.8.0 languageName: unknown linkType: soft @@ -3939,7 +4110,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": +"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -4790,6 +4961,18 @@ __metadata: languageName: node linkType: hard +"docusaurus-plugin-sass@npm:^0.2.3": + version: 0.2.3 + resolution: "docusaurus-plugin-sass@npm:0.2.3" + dependencies: + sass-loader: ^10.1.1 + peerDependencies: + "@docusaurus/core": ^2.0.0-beta + sass: ^1.30.0 + checksum: 928b02fd7c63835d54cdcce2eeda01cb6124707c6d43ef8e77db06474d3e3b4b7282cc0d88fed378cf1f3db2fc93ba8c11366965bb9b668eae19ca805e578acb + languageName: node + linkType: hard + "dom-converter@npm:^0.2.0": version: 0.2.0 resolution: "dom-converter@npm:0.2.0" @@ -5114,10 +5297,10 @@ __metadata: languageName: node linkType: hard -"eta@npm:^1.12.3": - version: 1.12.3 - resolution: "eta@npm:1.12.3" - checksum: 390c1cd320755cb98fd5a4a911539e8ed498fc49b82414f0023033ff606d80a34e6df0aeeb9fb0b519b318a750e6d17a72fc25f8a8c686cfc52d638e998237a1 +"eta@npm:^2.0.0": + version: 2.0.0 + resolution: "eta@npm:2.0.0" + checksum: ab8e93af73f0d4917485976aa8fcac68e730c47e9aa54a720c21c2e9087cdcd5f984a50cf5e04d189757612df014a229fa047a437651c9eea31e0b6bf1afe56b languageName: node linkType: hard @@ -6051,9 +6234,9 @@ __metadata: linkType: hard "http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0": - version: 4.1.0 - resolution: "http-cache-semantics@npm:4.1.0" - checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 languageName: node linkType: hard @@ -6214,6 +6397,13 @@ __metadata: languageName: node linkType: hard +"immutable@npm:^4.0.0": + version: 4.2.2 + resolution: "immutable@npm:4.2.2" + checksum: 4d6437ea9388fe8ceca7eed5c768cf438cda7fa14d2831b87b90aa00cc60d536964d107c255b8a2e5dbf4f44a0e1295afbb9d1f0a65fb4f57b936e71df601862 + languageName: node + linkType: hard + "import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -6622,6 +6812,13 @@ __metadata: languageName: node linkType: hard +"javascript-stringify@npm:^2.0.1": + version: 2.1.0 + resolution: "javascript-stringify@npm:2.1.0" + checksum: 009981ec84299da88795fc764221ed213e3d52251cc93a396430a7a02ae09f1163a9be36a36808689681a8e6113cf00fe97ec2eea2552df48111f79be59e9358 + languageName: node + linkType: hard + "jest-util@npm:^29.3.1": version: 29.3.1 resolution: "jest-util@npm:29.3.1" @@ -6793,10 +6990,10 @@ __metadata: languageName: node linkType: hard -"klona@npm:^2.0.5": - version: 2.0.5 - resolution: "klona@npm:2.0.5" - checksum: 8c976126ea252b766e648a4866e1bccff9d3b08432474ad80c559f6c7265cf7caede2498d463754d8c88c4759895edd8210c85c0d3155e6aae4968362889466f +"klona@npm:^2.0.4, klona@npm:^2.0.5": + version: 2.0.6 + resolution: "klona@npm:2.0.6" + checksum: ac9ee3732e42b96feb67faae4d27cf49494e8a3bf3fa7115ce242fe04786788e0aff4741a07a45a2462e2079aa983d73d38519c85d65b70ef11447bbc3c58ce7 languageName: node linkType: hard @@ -8361,11 +8558,11 @@ __metadata: linkType: hard "prettier@npm:^2.6.2": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" + version: 2.8.4 + resolution: "prettier@npm:2.8.4" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e languageName: node linkType: hard @@ -9214,6 +9411,44 @@ __metadata: languageName: node linkType: hard +"sass-loader@npm:^10.1.1": + version: 10.4.1 + resolution: "sass-loader@npm:10.4.1" + dependencies: + klona: ^2.0.4 + loader-utils: ^2.0.0 + neo-async: ^2.6.2 + schema-utils: ^3.0.0 + semver: ^7.3.2 + peerDependencies: + fibers: ">= 3.1.0" + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + sass: ^1.3.0 + webpack: ^4.36.0 || ^5.0.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + checksum: df9a65a62247e95305299ccbdf212cffdcdb69490928aecdf4f3dcf539b5302ed7cbffa663f83c5fc3ce0864decf84257a9ce484f6df4cb4426feeb88445dcd0 + languageName: node + linkType: hard + +"sass@npm:^1.57.1": + version: 1.58.3 + resolution: "sass@npm:1.58.3" + dependencies: + chokidar: ">=3.0.0 <4.0.0" + immutable: ^4.0.0 + source-map-js: ">=0.6.2 <2.0.0" + bin: + sass: sass.js + checksum: 35a2b98c037ef80fdc93c9b0be846e6ccc7d75596351a37ee79c397e66666d0a754c52c4696e746c0aff32327471e185343ca349e998a58340411adc9d0489a5 + languageName: node + linkType: hard + "sax@npm:^1.2.4": version: 1.2.4 resolution: "sax@npm:1.2.4" @@ -9603,7 +9838,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.0.2": +"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.2": version: 1.0.2 resolution: "source-map-js@npm:1.0.2" checksum: c049a7fc4deb9a7e9b481ae3d424cc793cb4845daa690bc5a05d428bf41bf231ced49b4cf0c9e77f9d42fdb3d20d6187619fc586605f5eabe995a316da8d377c @@ -9895,6 +10130,16 @@ __metadata: languageName: node linkType: hard +"swc-loader@npm:^0.2.3": + version: 0.2.3 + resolution: "swc-loader@npm:0.2.3" + peerDependencies: + "@swc/core": ^1.2.147 + webpack: ">=2" + checksum: 010d84d399525c0185d36d62c86c55ae017e7a90046bc8a39be4b7e07526924037868049f6037bc966da98151cb2600934b96a66279b742d3c413a718b427251 + languageName: node + linkType: hard + "tapable@npm:^1.0.0": version: 1.1.3 resolution: "tapable@npm:1.1.3" @@ -10092,10 +10337,30 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^4.9.4": + version: 4.9.5 + resolution: "typescript@npm:4.9.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db + languageName: node + linkType: hard + +"typescript@patch:typescript@^4.9.4#~builtin<compat/typescript>": + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=a1c5e5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 + languageName: node + linkType: hard + "ua-parser-js@npm:^0.7.30": - version: 0.7.32 - resolution: "ua-parser-js@npm:0.7.32" - checksum: 6b6b035dd78a0ab3369f166ab6f26225d823d83630788806d634f16259297a8f4ae6fe0be4e48f4353ac10dffded3971d7745c55d1432fdfc78a893ba58ef044 + version: 0.7.33 + resolution: "ua-parser-js@npm:0.7.33" + checksum: 1510e9ec26fcaf0d8c6ae8f1078a8230e8816f083e1b5f453ea19d06b8ef2b8a596601c92148fd41899e8b3e5f83fa69c42332bd5729b931a721040339831696 languageName: node linkType: hard @@ -10391,6 +10656,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -10877,6 +11151,17 @@ __metadata: languageName: node linkType: hard +"yaml-loader@npm:^0.8.0": + version: 0.8.0 + resolution: "yaml-loader@npm:0.8.0" + dependencies: + javascript-stringify: ^2.0.1 + loader-utils: ^2.0.0 + yaml: ^2.0.0 + checksum: d12dd264666b80baec23cea9f81cb677a9102d6f34ab45d8b6c085ace4d05b7285db9ce317db57264c3317af01128ce6e5b754e6866d15ccd75e8141902fb529 + languageName: node + linkType: hard + "yaml@npm:^1.10.0, yaml@npm:^1.10.2, yaml@npm:^1.7.2": version: 1.10.2 resolution: "yaml@npm:1.10.2" @@ -10884,6 +11169,13 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.0.0": + version: 2.2.1 + resolution: "yaml@npm:2.2.1" + checksum: 84f68cbe462d5da4e7ded4a8bded949ffa912bc264472e5a684c3d45b22d8f73a3019963a32164023bdf3d83cfb6f5b58ff7b2b10ef5b717c630f40bd6369a23 + languageName: node + linkType: hard + "yargs-parser@npm:^18.1.2": version: 18.1.3 resolution: "yargs-parser@npm:18.1.3" diff --git a/microsite/blog/2020-03-18-what-is-backstage.md b/microsite/blog/2020-03-18-what-is-backstage.md index 2c2b81d64d..5e3ec4ac90 100644 --- a/microsite/blog/2020-03-18-what-is-backstage.md +++ b/microsite/blog/2020-03-18-what-is-backstage.md @@ -85,4 +85,4 @@ We are envisioning [three phases](https://github.com/backstage/backstage/milesto https://github.com/backstage/backstage/milestones -Want to discuss the project or need support? Join us on [Discord](https://discord.gg/MUpMjP2) or reach out on [backstage-interest@spotify.com](mailto:backstage-interest@spotify.com). +Want to discuss the project or need support? Join us on [Discord](https://discord.gg/backstage-687207715902193673) or reach out on [backstage-interest@spotify.com](mailto:backstage-interest@spotify.com). diff --git a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md index 17f095f1fb..7d5098a721 100644 --- a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md +++ b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md @@ -125,4 +125,4 @@ If you are developing a plugin that might be useful for others, consider releasi ## Ready to get started? -Head over to GitHub and check out the [project](https://github.com/backstage/backstage) or download our [CLI](https://www.npmjs.com/package/@backstage/cli). If you have more questions, join us on [Discord](https://discord.gg/MUpMjP2) or [create an issue](https://github.com/backstage/backstage/issues/new/choose). +Head over to GitHub and check out the [project](https://github.com/backstage/backstage) or download our [CLI](https://www.npmjs.com/package/@backstage/cli). If you have more questions, join us on [Discord](https://discord.gg/backstage-687207715902193673) or [create an issue](https://github.com/backstage/backstage/issues/new/choose). diff --git a/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md b/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md index 7f8fe01e11..0f324cd5be 100644 --- a/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md +++ b/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md @@ -49,6 +49,6 @@ Alpha is our shorthand for "we don’t yet think Backstage is ready for producti As with most alpha releases, you should expect things to change quite a lot until we reach the beta stage (we’re targeting the end of summer). There are obviously many things missing as well, but we wanted to start collecting feedback early and make it easier to see the end-to-end flow. -If you have feedback or questions, please open a [GitHub issue](https://github.com/backstage/backstage/issues), ping us on [Discord chat](https://discord.gg/EBHEGzX) or send us an email at [backstage-interest@spotify.com](mailto:backstage-interest@spotify.com) 🙏 +If you have feedback or questions, please open a [GitHub issue](https://github.com/backstage/backstage/issues), ping us on [Discord chat](https://discord.gg/backstage-687207715902193673) or send us an email at [backstage-interest@spotify.com](mailto:backstage-interest@spotify.com) 🙏 To get regular product updates and news about the Backstage community, sign up for the [Backstage newsletter](https://info.backstage.spotify.com/newsletter_subscribe). diff --git a/microsite/blog/2020-08-05-announcing-backstage-software-templates.md b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md index 18bfa64c19..6acefa571c 100644 --- a/microsite/blog/2020-08-05-announcing-backstage-software-templates.md +++ b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md @@ -73,6 +73,6 @@ Backstage ships with four example templates, but since these are likely not the We have learned that one of the keys to getting these standards adopted is to keep an open process. Templates are code. By making it clear to your engineers that you are open to pull requests, and that teams with different needs can add their own templates, you are on the path of striking a good balance between autonomy and standardization. -If you have feedback or questions, please open a [GitHub issue](https://github.com/backstage/backstage/issues), ping us on [Discord chat](https://discord.gg/EBHEGzX) or send us an email at [backstage-interest@spotify.com](mailto:backstage-interest@spotify.com) 🙏 +If you have feedback or questions, please open a [GitHub issue](https://github.com/backstage/backstage/issues), ping us on [Discord chat](https://discord.gg/backstage-687207715902193673) or send us an email at [backstage-interest@spotify.com](mailto:backstage-interest@spotify.com) 🙏 To get regular product updates and news about the Backstage community, sign up for the [Backstage newsletter](https://info.backstage.spotify.com/newsletter_subscribe). diff --git a/microsite/blog/2021-01-12-new-backstage-feature-kubernetes-for-service-owners.md b/microsite/blog/2021-01-12-new-backstage-feature-kubernetes-for-service-owners.md index 8981f276be..7b0686aae7 100644 --- a/microsite/blog/2021-01-12-new-backstage-feature-kubernetes-for-service-owners.md +++ b/microsite/blog/2021-01-12-new-backstage-feature-kubernetes-for-service-owners.md @@ -70,4 +70,4 @@ To contribute or get more information on Kubernetes in Backstage, [join the disc [here]: https://github.com/backstage/backstage/issues?q=is%3Aissue+is%3Aopen+kubernetes+label%3Ak8s-plugin [read the docs]: https://backstage.io/docs/features/kubernetes/overview -[join the discussion on discord]: https://discord.gg/MUpMjP2 +[join the discussion on discord]: https://discord.gg/backstage-687207715902193673 diff --git a/microsite/blog/2021-06-22-spotify-backstage-is-growing.md b/microsite/blog/2021-06-22-spotify-backstage-is-growing.md index a12ddf7443..35285c5e04 100644 --- a/microsite/blog/2021-06-22-spotify-backstage-is-growing.md +++ b/microsite/blog/2021-06-22-spotify-backstage-is-growing.md @@ -49,7 +49,7 @@ We’ve launched a new website at: [backstage.spotify.com](https://backstage.spo You’ll find a high-level introduction to the platform, tips and tricks tested by Spotify to accelerate developer effectiveness, and access to a group of partners that have scaled Backstage for numerous adopters. You can also use the site to book product overviews, demos, and technical deep dives with members of the Spotify team. -We will continue to post important product announcements, technical documentation, feature demos, and community news here on Backstage.io. ([Subscribe to the newsletter](https://info.backstage.spotify.com/newsletter_subscribe) to stay up to date.) And both contributors and adopting companies can continue to find around-the-clock/around-the-world technical support on [GitHub](https://github.com/backstage/backstage) and [Discord](https://discord.gg/MUpMjP2). +We will continue to post important product announcements, technical documentation, feature demos, and community news here on Backstage.io. ([Subscribe to the newsletter](https://info.backstage.spotify.com/newsletter_subscribe) to stay up to date.) And both contributors and adopting companies can continue to find around-the-clock/around-the-world technical support on [GitHub](https://github.com/backstage/backstage) and [Discord](https://discord.gg/backstage-687207715902193673). ## Separate community sessions for adopters and contributors diff --git a/microsite/blog/2021-06-24-announcing-backstage-search-platform.md b/microsite/blog/2021-06-24-announcing-backstage-search-platform.md index 6914077522..118ac1f43c 100644 --- a/microsite/blog/2021-06-24-announcing-backstage-search-platform.md +++ b/microsite/blog/2021-06-24-announcing-backstage-search-platform.md @@ -99,4 +99,4 @@ We’ve built the foundation for the Backstage Search platform, and we can't wai You can check out our [project roadmap](https://backstage.io/docs/features/search/search-overview#project-roadmap) in our search documentation or track the progress of our [Beta milestone](https://github.com/backstage/backstage/milestone/27) and [GA milestone](https://github.com/backstage/backstage/milestone/28). -For any questions, feedback or ideas about the Backstage Search platform, join us in the #search channel on [Discord](https://discord.gg/MUpMjP2)! +For any questions, feedback or ideas about the Backstage Search platform, join us in the #search channel on [Discord](https://discord.gg/backstage-687207715902193673)! diff --git a/microsite/blog/2021-09-16-the-techdocs-beta-has-landed.md b/microsite/blog/2021-09-16-the-techdocs-beta-has-landed.md index f3f79abf44..27df0b3a44 100644 --- a/microsite/blog/2021-09-16-the-techdocs-beta-has-landed.md +++ b/microsite/blog/2021-09-16-the-techdocs-beta-has-landed.md @@ -61,4 +61,4 @@ If you're upgrading from a prior, alpha version of TechDocs, be aware that there - If you've configured TechDocs to publish generated content to a remote storage solution provider like AWS S3, Google Cloud Storage, or Azure Blob Storage, you will need to use the TechDocs CLI to migrate the contents of your storage bucket. [See full migration instructions here](https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-alpha-to-beta). - In order to take advantage of the stale file deletion functionality, you may need to add some permissions to the account or role used by your TechDocs backend to publish contents to your storage provider. [See "Using Cloud Storage docs"](https://backstage.io/docs/features/techdocs/using-cloud-storage) for updated permissions recommendations. -Thanks again to the whole community for hitting this milestone with us! Catch us in [Discord](https://discord.gg/MUpMjP2) or an [upcoming community session](https://github.com/backstage/community) if you have any questions. +Thanks again to the whole community for hitting this milestone with us! Catch us in [Discord](https://discord.gg/backstage-687207715902193673) or an [upcoming community session](https://github.com/backstage/community) if you have any questions. diff --git a/microsite/blog/2022-03-04-backstage-upgrade-helper.md b/microsite/blog/2022-03-04-backstage-upgrade-helper.md index 80b3b9c0a8..3f4fc6da77 100644 --- a/microsite/blog/2022-03-04-backstage-upgrade-helper.md +++ b/microsite/blog/2022-03-04-backstage-upgrade-helper.md @@ -13,7 +13,7 @@ _TLDR;_ The Spotify team recently announced [Backstage Upgrade Helper](https://b ## Upgrading Backstage was a common pain point -Without a doubt, one of the most exciting things about Backstage is our community. Step into our daily [Discord conversations](https://discord.gg/sBEF5VkG) and the monthly [Community Sessions](https://www.youtube.com/watch?v=0QMQYSTKAx0) and you’ll find a ton of great ideas, problem-solving, and support for one another. +Without a doubt, one of the most exciting things about Backstage is our community. Step into our daily [Discord conversations](https://discord.gg/backstage-687207715902193673) and the monthly [Community Sessions](https://www.youtube.com/watch?v=0QMQYSTKAx0) and you’ll find a ton of great ideas, problem-solving, and support for one another. During one of these discussions, we realized we all shared the same pain point — upgrading a Backstage application. diff --git a/microsite/blog/2022-03-17-backstage-1.0.md b/microsite/blog/2022-03-17-backstage-1.0.md index 02ead0390c..8cc627c127 100644 --- a/microsite/blog/2022-03-17-backstage-1.0.md +++ b/microsite/blog/2022-03-17-backstage-1.0.md @@ -54,6 +54,6 @@ If you want to know more about this first major release, check out the following - [Release notes](https://backstage.io/docs/releases/v1.0.0) and [Changelog](https://github.com/backstage/backstage/releases/tag/v1.0.0). - [Backstage official documentation](https://backstage.io/docs/) and [backstage.spotify.com](https://backstage.spotify.com/). -- [Discord channel](https://discord.gg/EUaBAS58). +- [Discord channel](https://discord.gg/backstage-687207715902193673). - [Backstage repository on GitHub](https://github.com/backstage/backstage). - [List of public adopters](https://github.com/backstage/backstage/blob/master/ADOPTERS.md). diff --git a/microsite/blog/2022-12-19-backstage-wrapped-2022.md b/microsite/blog/2022-12-19-backstage-wrapped-2022.md index 967bc14433..4986b3effd 100644 --- a/microsite/blog/2022-12-19-backstage-wrapped-2022.md +++ b/microsite/blog/2022-12-19-backstage-wrapped-2022.md @@ -20,7 +20,7 @@ Last year we celebrated 75 [public adopters](https://github.com/backstage/backst ![Accelerated growth of Public Backstage Adopters, from January 2021–December 2022.](assets/2022-12-19/public-backstage-adopters-2022.png) -Not only are our adopters growing at an exponential rate, but so too are our contributors. Our [Discord server](https://discord.gg/backstage-687207715902193673) is a perfect example of the worlds of our adopters and contributors melding. On average, at any given hour of the day or night, there are more than 500 people online available to talk and provide support on any Backstage topic. Discord discussions run a full gamut of topics – from technical support questions to best practices for onboarding engineers to Backstage – basically, anything and everything an adopter may be interested to know and learn. At the very beginning of 2022, we “only” had 4k Discord members. Today, we have a growing membership of 8.2k. +Not only are our adopters growing at an exponential rate, but so too are our contributors. Our [Discord server](https://discord.gg/backstage-687207715902193673-687207715902193673) is a perfect example of the worlds of our adopters and contributors melding. On average, at any given hour of the day or night, there are more than 500 people online available to talk and provide support on any Backstage topic. Discord discussions run a full gamut of topics – from technical support questions to best practices for onboarding engineers to Backstage – basically, anything and everything an adopter may be interested to know and learn. At the very beginning of 2022, we “only” had 4k Discord members. Today, we have a growing membership of 8.2k. Isn’t it what we can call a great and vibrant community of enthusiasts? diff --git a/microsite/blog/2023-01-31-incremental-entity-provider.md b/microsite/blog/2023-01-31-incremental-entity-provider.md new file mode 100644 index 0000000000..fd273af208 --- /dev/null +++ b/microsite/blog/2023-01-31-incremental-entity-provider.md @@ -0,0 +1,45 @@ +--- +title: Scaling Backstage Ingestion with Incremental Entity Providers +author: Paul Cowan & Taras Mankovski +authorURL: https://frontside.com/ +--- + +At the heart of [Backstage](https://backstage.io/) is the [Backstage Software Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview), which is a data store that allows an organization to centralize and visualize its many software services and components. Backstage inspects and transforms an organization's disparate software services and parts into a centralized data store. This blog post introduces the concept of incremental entity providers, which allow Backstage to scale ingestion to even larger datasets. + +![catalog pipeline](assets/2023-01-31/catalog-pipeline.png) + +<!--truncate--> + +A common use case is for an organization to want to surface ownership and metadata about repositories. Backstage provides a mechanism for discovering and transforming repository information into a standard data structure and persisting it into the Backstage [Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview). This process is known as ingestion, where all data is transformed into a standard Backstage data structure known as an entity. Entities in the Catalog’s data store are accessible to the Backstage App via the REST API. + +Data is transformed into entities via what is known as the ingestion and processing loop, which can be thought of as an [extract, transform and load (ETL) pipeline](https://en.wikipedia.org/wiki/Extract,_transform,_load), where raw data such as GitHub repositories are loaded from GitHub, transformed into entities and outputted to the Catalog. + +## Entity Providers + +Backstage offers what are known as [entity providers](https://backstage.io/docs/features/software-catalog/life-of-an-entity) as a means for ingesting the raw data into the pipeline and transforming them into Backstage entities. For example, Backstage comes with a [GitHub Entity Provider](https://backstage.io/docs/reference/plugin-catalog-backend-module-github) that finds all catalog-info.yaml files in GitHub repositories. The processing loop transforms them into Backstage entities and subsequently persists them to the software catalog. + +Entity providers are a relatively new abstraction and the recommended way to ingest data into the catalog. The Backstage catalog engine starts each registered entity provider, which connects to its data source (e.g., the GitHub Entity Provider connects to GitHub). The entity provider will query the external data source and convert the data into the entity format. Finally, the entity provider issues what is known as a mutation to the catalog engine. A mutation is a signal from the entity provider to the catalog engine that entities are available to be processed and stored. + +A mutation can be either a full mutation or a delta mutation. A full mutation replaces all entities previously ingested by the entity provider with a new set of entities. The entity provider will remove all entities not found in the latest ingestion. A full mutation can be used to ingest relatively small datasets (less than 10,000 entities); however, ingesting more during a full ingestion may cause out-of-memory errors and delay the processing of entities from other entity providers. A delta mutation can surgically add and remove entities from the catalog. A delta mutation is useful when the data source provides events-based APIs like webhooks, which allows the Backstage catalog engine to ingest a small number of entities as they get added, updated and/or deleted. + +## Incremental entity providers + +A large organization typically deals with massive datasets. Until recently, ingesting large datasets with entity providers has been problematic because performing a full ingestion resulted in out-of-memory errors, and many data sources don’t provide webhooks or other events-based APIs. At the same time, the datasets were too large to efficiently manage through targeted delta mutations. + +This is a problem that [DevEx team at HP](http://hp.com) faced when building their software catalog with Backstage. [Damon Kaswell](https://github.com/dekoding), Senior Application Developer on the DevEx team at HP, shared their experience at [BackstageCon 2022](https://www.youtube.com/watch?v=5qHyZntKXRU&list=PLj6h78yzYM2OKySsTuiip3BqmdYZQRnSf&index=13), detailing the problem and the solution that [Frontside](https://frontside.com/) created in collaboration with developers on HP’s DevEx team. + +![Damon Kaswell](assets/2023-01-31/damon.jpg) + +The solution HP and [Frontside](https://frontside.com/) arrived at was to implement an incremental entity provider. An incremental entity provider effectively performs a full mutation using a series of delta mutations combined with a mark and sweep mechanism. It paginates through the dataset, tracking entities retrieved from each page and the cursor of the next page, pausing ingestion every few seconds to give the processing loop time to process existing entities. Once it reaches the end of the dataset, it determines which entities were not ingested during this ingestion cycle and emits a delta mutation to delete unmarked entities. + +Simply by adding a few new tables to the database schema, the incremental ingestion entity provider converts any existing entity provider into an incremental entity provider. These tables allow the incremental entity provider to be long-lived and keep track of its current location in the dataset by persisting a cursor that it uses to page through any large dataset. The larger the dataset, the more pages of data or bursts of work the incremental entity provider will ingest—but there will be no out-of-memory errors, effectively removing scalability problems. + +The results speak for themselves. Migrating from regular entity providers to incremental entity providers reduced ingestion time by 92% – from over 4 and a half hours to just 20 minutes. Incremental entity providers eliminated the ingestion maintenance burden from being a constant problem to a non-issue. Writing reliable integration with external services can now be done in days instead of weeks. + +## Go forth and ingest! + +Backstage provides a robust framework for ingesting data from external sources, but HP needed to scale it beyond its design. The Backstage framework allowed [Frontside](https://frontside.com/) and HP’s developers to extend it with a plugin to support HP’s scaling requirements. + +We're delighted to share that as of [this PR](https://github.com/backstage/backstage/pull/14356), the incremental ingestion backend is available for anyone to use with Backstage. The solution was released open source as [@backstage/plugin-catalog-backend-module-incremental-ingestion](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-incremental-ingestion#backstageplugin-catalog-backend-module-incremental-ingestion) and contains a package for creating incremental entity providers. The plugin's [repository README](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-incremental-ingestion) has detailed configuration and usage outlined. + +The incremental ingestion entity provider is an excellent addition to the Backstage stack. Battle-tested on large datasets, the incremental entity provider is a significant step forward in smoothing the path to successful ingestion at scale. diff --git a/microsite/blog/2023-02-06-whats-ahead-in-2023.md b/microsite/blog/2023-02-06-whats-ahead-in-2023.md new file mode 100644 index 0000000000..f989973ca0 --- /dev/null +++ b/microsite/blog/2023-02-06-whats-ahead-in-2023.md @@ -0,0 +1,60 @@ +--- +title: What’s Ahead in 2023? +author: Lee Mills +authorURL: https://github.com/leemills83 +--- + +# What’s Ahead in 2023? + +![Fewer “vibes” and more climbs (up the contribution ladder)](assets/2023-02-06/whats-ahead.png) + +Okay, everyone. Time to move on from all the 2022 reflections, Top 10 lists, and Wrapped(s), and turn our gaze in the other direction — towards 2023. As the Backstage team moves into the new year, we find ourselves asking those perennial questions: How can we continue to grow and enhance the Backstage community? How can we improve the experience for those adopting Backstage? How can we give the best support to Backstage contributors? + +<!--truncate--> + +Well, we don’t have all the answers, but we do have some. So we wanted to share more on three ongoing initiatives aimed at moving the needle forward for these three distinct, yet interrelated, questions. + +- Grow your influence by climbing the contribution ladder +- Come join our SIGs (and push for new ones) +- Work with us to drive a better experience for Backstage adopters + +# Climb the contribution ladder + +Truth be told, we’ve been wanting to grow the maintainer team for some time, and have had multiple requests from contributors and contributing organizations wanting to become maintainers. But, so far, we just haven’t managed to get anything off the ground. What we see is that becoming a maintainer is too hard and the current ownership model doesn’t work. In short, we need to update Backstage governance. + +To address this, we are proposing (RFC links below) the following three initiatives: + +- Introduce more steps in between first-time contributor and maintainer in the form of a contributor ladder (loosely based on the [CNCF Project Template](https://github.com/cncf/project-template/blob/main/CONTRIBUTOR_LADDER.md)). The ladder might look as follows: Contributor, Org Member, Reviewer, Maintainer, Project Maintainer. +- Change the ownership model, for example, divide the project up into several project areas, each covering particular parts of the project. +- Introduce a new process for large contributions in the form of Backstage Enhancement Proposals (BEPs). + +Read more and join the already rich discussion in [RFC: Updated Backstage Governance](https://github.com/backstage/backstage/issues/15317) and [RFC: Backstage Enhancement Proposals](https://github.com/backstage/backstage/issues/15318). The RFC will be open until 13/02/2023. + +# Come join our SIGs (and push for new ones) + +As many of you know, SIG stands for Special Interest Group. The purpose of a SIG is to bring together maintainers and interested contributors within a certain specialist area to discuss, debate, spread knowledge, brainstorm - and drive the overall project forward. They’re also a lot of fun! + +We currently have two SIGs up and running: + +- [Catalog](https://github.com/backstage/community/tree/main/sigs/sig-catalog?utm_campaign=Newsletters%20Sent&utm_medium=email&_hsmi=241130993&_hsenc=p2ANqtz-9fx-nTmQga2j12NqzZaL-lYPq-xQjDoXcWKe_JUoa2RGlK65R9j0CIygfYILr0tGEQSvRVN9xzhiJ6LmVxBthKnStfSA&utm_content=241130993&utm_source=hs_email) started up last June and covers all aspects of the systems and components related to the Software Catalog. The group runs every second Tuesday between 16:00 and 17:00 CET. Just show up or hop into [#catalog](https://discord.com/channels/687207715902193673/923144214580191282) (Discord), if you want to talk to us first. Here are the [notes from our sessions so far](https://docs.google.com/document/d/1xg57CDfX2R5jTGbbWANzifm2szEb664kuTwfEEvKgZU/edit#heading=h.iw0hjv6xfb9e). +- [Adoption](https://github.com/backstage/community/tree/main/sigs/sig-adoption?utm_campaign=Newsletters%20Sent&utm_medium=email&_hsmi=241130993&_hsenc=p2ANqtz-9CAxtT8JSeJZPh41o3ZnDBi0VBFpbL6fEqXInr9HHOFhU3JAo8HekKg1JzE5rhGP5zOaHcsYDd1YxJWNHw-eSk5mLWkA&utm_content=241130993&utm_source=hs_email) kicked off in December and covers all aspects of the Backstage adoption journey including documentation and tutorials. In the first two meetups, we discussed adoption challenges based on user persona, collecting data about those challenges, and bootstrapping the Catalog to improve metadata quality for adopters. This one runs every second Thursday between 16:00 and 17:00 CET. Again just show up or, if you want to talk to us first, you can do so in [#adoption](https://discord.com/channels/687207715902193673/1045031039061479536) (Discord). Here are the [notes from our sessions](https://docs.google.com/document/d/1zeYSzXYgh897bxPQmwy2btOwCFlEjlUDLf00riySLRc/edit?_hsmi=241130993&_hsenc=p2ANqtz-_nPuE5hW4fnbhJ5-uK8r5HisSTU7rkslKirvqu0g-vdynbitPVYF2AVLAatos2dLoxb2Bgx_g4LBgH02kXoq7_Acd43Q#heading=h.iw0hjv6xfb9e) so far. + +So that’s SIGs. Get involved and join the ones we have up and running. And/or feel free to propose new ones. + +# Help drive a better experience for all adopters + +Throughout all stages of adopting Backstage in an organization — initial discovery, evaluation and POC, and scaling to the wider org — we have identified opportunities where we want to make the journey for Backstage adopters faster and smoother. + +We’ll go step-by-step, but initial plans are to: + +- Make it easier to install, customize, and maintain Backstage +- Provide Golden Paths for adoption +- Improve the experience and the content on [backstage.io](https://backstage.io/docs/overview/what-is-backstage) + +As we move forward, we’d love you to be part of it. Here are some ways that you can contribute to this initiative: + +- Simply give us feedback. [Tell us about your adoption journey](https://spotify.co1.qualtrics.com/jfe/form/SV_cuxhYPB6oQDu7gW). +- Contribute to the pages and documentation on [backstage.io](https://backstage.io/docs/overview/what-is-backstage) +- Pay it forward by making code contributions even during your adoption journey (if you are on one) +- Join the [Adoption](https://github.com/backstage/community/tree/main/sigs/sig-adoption?utm_campaign=Newsletters%20Sent&utm_medium=email&_hsmi=241130993&_hsenc=p2ANqtz-9CAxtT8JSeJZPh41o3ZnDBi0VBFpbL6fEqXInr9HHOFhU3JAo8HekKg1JzE5rhGP5zOaHcsYDd1YxJWNHw-eSk5mLWkA&utm_content=241130993&utm_source=hs_email) SIG +- Chat with us on [#adoption](https://discord.com/channels/687207715902193673/1045031039061479536) (Discord) diff --git a/microsite/blog/2023-02-15-backend-system-alpha.md b/microsite/blog/2023-02-15-backend-system-alpha.md new file mode 100644 index 0000000000..9970945f67 --- /dev/null +++ b/microsite/blog/2023-02-15-backend-system-alpha.md @@ -0,0 +1,83 @@ +--- +title: Backstage Backend System Alpha +author: Patrik Oldsberg +authorURL: https://github.com/Rugvip +authorImageURL: https://avatars.githubusercontent.com/u/4984472?v=4 +--- + +![backend system alpha header image](assets/2023-02-15/backend-system-alpha.gif) + +For the past year, the Backstage maintainer team has been spending time to give the [old Backstage backend system](https://backstage.io/docs/plugins/backend-plugin) a much needed revamp. Our goal was not to build something completely new from scratch, but rather to solidify the existing foundations that have grown organically. We wanted to make plugin installation a lot simpler, while maintaining and even improving the ways in which you can customize your backend setups. + +We’re happy to announce that the [v1.11](https://backstage.io/docs/releases/v1.11.0) release includes the public alpha of the [new Backstage backend system](https://backstage.io/docs/backend-system/)! The system has already been available to use for some time, as some of you have already found, but this alpha release marks the point where we are finally ready to encourage widespread adoption by plugin builders. We still don’t recommend that you use this new system in your production setups just yet, more on that later. + +<!--truncate--> + +## History of the Backstage backend system + +The backend system that we all use today has grown very organically based on all the needs that have been discovered along the journey of the Backstage Open Source project. We started out with a very flexible system that was essentially just a collection of recommended patterns for plugins, along with a couple of common interfaces and utilities. This was largely due to the fact that before it was open sourced, the Backstage backend services at Spotify were all built on top of internal frameworks and practices that we couldn’t bring with us. Back in 2020, we didn’t have a lot of knowledge about what a good backend system for Backstage would look like, so we started off with a highly flexible foundation that would evolve organically over time. + +As Backstage grew across all dimensions of adopter count, community size, code size, and capability, we saw the need to take a step back and reevaluate how Backstage backend plugins are built. About one year ago, the maintainers sat down and started to sketch what a new backend system could look like. + +## Designing the New System + +When we set out on this project, we had a few primary goals. First, we intended to introduce as few new concepts as possible. Our existing system already had a lot of different patterns, and ideally we wanted to only give those patterns names and make them part of the framework. Second, we wanted to keep the backend system recognizable to those familiar with the frontend system. The list of goals goes on, we had a lot of reasons to build this new system, these are a few more important ones: + +- Make it easier to create and maintain backend installations. +- Align how plugins provide points of customization and how those customizations are installed. +- Make it much easier to maintain plugins, in particular keeping the API stable. +- Simplify the process of splitting plugins out into separate deployments with shared environments. +- Improve the local development and testing experience. + +Prioritizing simplicity is often a guiding principle that we use for designs that span multiple ownership roles. We decide on which parts of the system that we think are the most important to have as simple as possible, or, viewed from the opposite end, where in the system we put necessary complexity. In this case, we optimized for keeping the backend setup as simple as possible, followed by modules and plugins, then libraries, and lastly the framework itself. What this guidance means in practice is that when there is complexity that needs to be added to implement a certain feature, we place as much of it as possible within the framework itself, then libraries, plugins and modules, and if absolutely needed, the backend setup. + +To get the Backstage backend system to where we are today, we went through three major stages, starting with some very early high level experimentation. If you’re a fan of Git archaeology, you can see some of our early ideas in [our experiments branch](https://github.com/backstage/backstage/commit/fd3e6b67fef25a04789779700db4c37047d59d1b), as well as the [separate repository](https://github.com/backstage/backend-system-exploration) that we used to explore different ideas together with [Roadie](https://roadie.io/). The output of this work was all presented in an [RFC](https://github.com/backstage/backstage/issues/11611) to gather feedback. + +A few months later as the RFC settled, we resumed work and followed up with an initial implementation. At this point there were still a lot of details in the design to figure out, especially as we started looking at how our existing plugins and services fit into this new system. We identified a lot of aspects that we inevitably didn’t consider in the initial design, and started exploring technical solutions to those problems. There was a lot of zooming in and out, both implementing details and then stepping back and looking at how that affects the broader system and where to go next. This ended up being the most time consuming stage, and we spent several months iterating on this. + +We wrapped up this work just before KubeCon NA 2022, with the full system implemented. The state of the system at this point was presented in our talk on the topic, which you can see just below. Except for a few API tweaks, this is what the system still looks like today. + +<iframe width="780" height="440" src="https://www.youtube.com/embed/ZXF7uxrEBVY" frameBorder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe> + +<br/> + +The final stage was all about polish, which we picked up at the beginning of this year. By leaving a bit of space between the main implementation, or phase two, and the polish phase, phase three, it was much easier to properly scrutinize our past solutions. We forget a lot of the low level reasoning for changes, and it is easier to step back and reconsider parts of the system. For example, we found that our initial design of the `createServiceFactory` function was unnecessarily complex, and could be made much simpler for most usages. In addition to reviewing all the APIs, we also wrote more documentation to help users, ensured that the local development and testing experience was solid, and double checked that there was a smooth migration path from the old system. + +## Architecture + +Now that you’ve learned about the journey of designing the new system, let’s review the new architecture. This blog includes a brief introduction to the architecture of the new system. If you want to dive deeper, check out our [architecture documentation](https://backstage.io/docs/backend-system/architecture/). + +The backend system has five major building blocks: + +- Backend instances act as dependency injection containers and are the unit of deployment in which you install plugins and modules that you want to use. +- Plugins implement the actual features and main business logic, for example the service catalog. +- Services provide shared utilities for plugins to use, for example logging, database access, and serving HTTP. Services are provided by the backend and can be customized in each backend instance. +- Extension Points serve as an optional API surface for plugins to provide deeper customization, such as installing custom processors in the catalog. It is up to each plugin to define their own extension points. +- Modules extend individual plugins with additional features by interacting with the extension points that the plugins expose. A module might for example implement a single entity provider for the catalog. + +![backend system architecture](assets/2023-02-15/backend-system-architecture.png) + +## Benefits of the New System + +This new system reduced the size of a typical backend installation from hundreds of lines of code to just about two dozen, more than an order of magnitude. This reduction comes from shifting complexity away from the backend installation and into the plugins, services, and the backend system itself. Not only does this make a typical backend setup much easier to maintain, but it also makes it much easier to build plugins and modules, and leaves fewer patterns to be unnecessarily invented. + +As part of this shift, we’ve moved many concepts from being coding patterns to being features built into the framework. This will allow us to further evolve the backend system in much more interesting ways, and still bring along the entire Backstage plugin ecosystem. It also helps align Backstage plugins around a couple of extensibility patterns, rather than each plugin having their own bespoke patterns for how extensions are installed. + +## Alpha, what does it mean? + +With the release of the backend system into alpha, we are confident enough in the design of the system that we encourage plugin owners to add support for the new system in their plugins. This means following the [plugin migration guide](https://backstage.io/docs/backend-system/building-plugins-and-modules/migrating) and exporting a plugin instance. Do not remove support for the old system yet – keep your existing `createRouter` export intact. + +While we encourage migration of plugins, we do not yet recommend migrating your backend installations to the new system. We want to create space to gather and address feedback from the plugin migration experience, and to make changes if necessary. + +If you are worried about the upcoming backend migration, you can [take a peek](https://backstage.io/docs/backend-system/building-backends/migrating#migrating-the-index-file) at our `legacyPlugin` helper. It allows you to forklift over all existing plugin setups to the new backend system, and then migrate plugins one by one. + +## Next Steps + +During the next release we will keep working on migrating the core plugins, as well as addressing any feedback that we receive. We don’t want to rush a stable release though, and won’t encourage broad adoption until we’re confident that we won’t have to follow it up with immediate breaking changes. Yet, we are excited about where we are today, and are looking forward to the future stable release! + +Useful Links: + +- [Architecture Docs](https://backstage.io/docs/backend-system/architecture/) +- [Building Plugins](https://backstage.io/docs/backend-system/building-plugins-and-modules/) +- [Core Services](https://backstage.io/docs/backend-system/core-services/) +- [#backend-system](https://discord.com/channels/687207715902193673/1034089724664610938) channel in Discord diff --git a/microsite/blog/assets/2023-01-31/catalog-pipeline.png b/microsite/blog/assets/2023-01-31/catalog-pipeline.png new file mode 100644 index 0000000000..e21de99a7a Binary files /dev/null and b/microsite/blog/assets/2023-01-31/catalog-pipeline.png differ diff --git a/microsite/blog/assets/2023-01-31/damon.jpg b/microsite/blog/assets/2023-01-31/damon.jpg new file mode 100644 index 0000000000..f1e37d354a Binary files /dev/null and b/microsite/blog/assets/2023-01-31/damon.jpg differ diff --git a/microsite/blog/assets/2023-02-06/whats-ahead.png b/microsite/blog/assets/2023-02-06/whats-ahead.png new file mode 100644 index 0000000000..e49f68149d Binary files /dev/null and b/microsite/blog/assets/2023-02-06/whats-ahead.png differ diff --git a/microsite/blog/assets/2023-02-15/backend-system-alpha.gif b/microsite/blog/assets/2023-02-15/backend-system-alpha.gif new file mode 100644 index 0000000000..ca91651e9a Binary files /dev/null and b/microsite/blog/assets/2023-02-15/backend-system-alpha.gif differ diff --git a/microsite/blog/assets/2023-02-15/backend-system-architecture.png b/microsite/blog/assets/2023-02-15/backend-system-architecture.png new file mode 100644 index 0000000000..90c1a495fd Binary files /dev/null and b/microsite/blog/assets/2023-02-15/backend-system-architecture.png differ diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index 327e42f38a..c93b1779cf 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -40,7 +40,9 @@ class Footer extends React.Component { </div> <div> <h5>Community</h5> - <a href="https://discord.gg/MUpMjP2">Support chatroom</a> + <a href="https://discord.gg/backstage-687207715902193673"> + Support chatroom + </a> <a href="https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md"> Contributing </a> diff --git a/microsite/data/plugins/adr.yaml b/microsite/data/plugins/adr.yaml index fabb26e685..e6b7b0cccb 100644 --- a/microsite/data/plugins/adr.yaml +++ b/microsite/data/plugins/adr.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/kuangp category: Discovery description: Browse your project's ADRs. documentation: https://github.com/backstage/backstage/tree/master/plugins/adr -iconUrl: img/adr-logo.png +iconUrl: /img/adr-logo.png npmPackageName: '@backstage/plugin-adr' addedDate: '2022-04-13' diff --git a/microsite/data/plugins/announcements.yaml b/microsite/data/plugins/announcements.yaml index de80dcf6fe..d03c1a1bd7 100644 --- a/microsite/data/plugins/announcements.yaml +++ b/microsite/data/plugins/announcements.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/K-Phoen category: Discovery description: Write and share announcements within Backstage. documentation: https://github.com/K-Phoen/backstage-plugin-announcements/ -iconUrl: img/plugin-announcements-logo.png +iconUrl: /img/plugin-announcements-logo.png npmPackageName: '@k-phoen/backstage-plugin-announcements' addedDate: '2022-11-09' diff --git a/microsite/data/plugins/apollo-explorer.yaml b/microsite/data/plugins/apollo-explorer.yaml index 1a95eaa035..98008e3fc0 100644 --- a/microsite/data/plugins/apollo-explorer.yaml +++ b/microsite/data/plugins/apollo-explorer.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/unredundant category: Debugging description: Integrates Apollo Explorer graphs as a tool to browse GraphQL API endpoints inside Backstage. documentation: https://github.com/backstage/backstage/blob/master/plugins/apollo-explorer/README.md -iconUrl: img/apollo-explorer.png +iconUrl: /img/apollo-explorer.png npmPackageName: '@backstage/plugin-apollo-explorer' addedDate: '2022-07-20' diff --git a/microsite/data/plugins/azure-pipelines.yaml b/microsite/data/plugins/azure-pipelines.yaml index d4caa90507..5b719a4248 100644 --- a/microsite/data/plugins/azure-pipelines.yaml +++ b/microsite/data/plugins/azure-pipelines.yaml @@ -5,6 +5,6 @@ authorUrl: https://keyloop.com/ category: CI/CD description: Easily view your Azure Pipelines within the Software Catalog documentation: https://github.com/backstage/backstage/blob/master/plugins/azure-devops/README.md -iconUrl: img/azure-pipelines.svg +iconUrl: /img/azure-pipelines.svg npmPackageName: '@backstage/plugin-azure-devops' addedDate: '2021-12-22' diff --git a/microsite/data/plugins/azure-spring-apps.yml b/microsite/data/plugins/azure-spring-apps.yml index 5b41885ac4..e0b77a42c8 100644 --- a/microsite/data/plugins/azure-spring-apps.yml +++ b/microsite/data/plugins/azure-spring-apps.yml @@ -5,6 +5,6 @@ authorUrl: https://enfuse.io/ category: Discovery description: Easily view your Azure Spring Apps service resources documentation: https://github.com/enfuse/asae-backstage-plugin/blob/main/README.md -iconUrl: img/enfuse.png +iconUrl: /img/enfuse.png npmPackageName: '@enfuse/plugin-azure-spring-apps' addedDate: '2022-11-21' diff --git a/microsite/data/plugins/backstage-analytics-module-ga.yaml b/microsite/data/plugins/backstage-analytics-module-ga.yaml index 051345d43b..d58ca322eb 100644 --- a/microsite/data/plugins/backstage-analytics-module-ga.yaml +++ b/microsite/data/plugins/backstage-analytics-module-ga.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/spotify category: Analytics description: Track usage of your Backstage instance using Google Analytics. documentation: https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md -iconUrl: img/ga-icon.png +iconUrl: /img/ga-icon.png npmPackageName: '@backstage/plugin-analytics-module-ga' addedDate: '2021-10-07' diff --git a/microsite/data/plugins/backstage-kubernetes.yaml b/microsite/data/plugins/backstage-kubernetes.yaml index a070ea99b0..f9514880b0 100644 --- a/microsite/data/plugins/backstage-kubernetes.yaml +++ b/microsite/data/plugins/backstage-kubernetes.yaml @@ -4,8 +4,8 @@ author: Spotify authorUrl: https://github.com/spotify category: Core Feature description: Monitor all your service's deployments at a glance, even across clusters. -documentation: https://backstage.io/docs/features/kubernetes/overview -iconUrl: img/backstage-k8s.svg +documentation: https://backstage.io/docs/features/kubernetes/ +iconUrl: /img/backstage-k8s.svg npmPackageName: '@backstage/plugin-kubernetes' order: 4 addedDate: '2021-03-29' diff --git a/microsite/data/plugins/backstage-search-platform.yaml b/microsite/data/plugins/backstage-search-platform.yaml index 5a313ae9da..fa069d8875 100644 --- a/microsite/data/plugins/backstage-search-platform.yaml +++ b/microsite/data/plugins/backstage-search-platform.yaml @@ -4,8 +4,8 @@ author: Spotify authorUrl: https://github.com/spotify category: Core Feature description: A composable and extensible search platform built to fit your organization’s needs and find information quickly. -documentation: https://backstage.io/docs/features/software-catalog/software-catalog-overview -iconUrl: img/backstage-search-platform.svg +documentation: https://backstage.io/docs/features/search/ +iconUrl: /img/backstage-search-platform.svg npmPackageName: '@backstage/plugin-search' order: 5 addedDate: '2022-09-07' diff --git a/microsite/data/plugins/backstage-software-catalog.yaml b/microsite/data/plugins/backstage-software-catalog.yaml index e633e17e8e..3d47acabc7 100644 --- a/microsite/data/plugins/backstage-software-catalog.yaml +++ b/microsite/data/plugins/backstage-software-catalog.yaml @@ -4,8 +4,8 @@ author: Spotify authorUrl: https://github.com/spotify category: Core Feature description: Manage all your services and software components, all in one place. -documentation: https://backstage.io/docs/features/software-catalog/software-catalog-overview -iconUrl: img/backstage-software-catalog.svg +documentation: https://backstage.io/docs/features/software-catalog/ +iconUrl: /img/backstage-software-catalog.svg npmPackageName: '@backstage/plugin-catalog' order: 1 addedDate: '2021-06-17' diff --git a/microsite/data/plugins/backstage-software-templates.yaml b/microsite/data/plugins/backstage-software-templates.yaml index f111ee30ce..916f754a2a 100644 --- a/microsite/data/plugins/backstage-software-templates.yaml +++ b/microsite/data/plugins/backstage-software-templates.yaml @@ -4,8 +4,8 @@ author: Spotify authorUrl: https://github.com/spotify category: Core Feature description: Create new software components in just a few steps, with your standards built-in (Scaffolder). -documentation: https://backstage.io/docs/features/software-templates/software-templates-index -iconUrl: img/backstage-software-templates.svg +documentation: https://backstage.io/docs/features/software-templates/ +iconUrl: /img/backstage-software-templates.svg npmPackageName: '@backstage/plugin-scaffolder' order: 2 addedDate: '2021-03-29' diff --git a/microsite/data/plugins/backstage-techdocs.yaml b/microsite/data/plugins/backstage-techdocs.yaml index 8d89ac5060..ea462a6c94 100644 --- a/microsite/data/plugins/backstage-techdocs.yaml +++ b/microsite/data/plugins/backstage-techdocs.yaml @@ -4,8 +4,8 @@ author: Spotify authorUrl: https://github.com/spotify category: Core Feature description: A docs-like-code solution to technical documentation. Write your docs right alongside your code. -documentation: https://backstage.io/docs/features/techdocs/techdocs-overview -iconUrl: img/backstage-techdocs.svg +documentation: https://backstage.io/docs/features/techdocs/ +iconUrl: /img/backstage-techdocs.svg npmPackageName: '@backstage/plugin-techdocs' order: 3 addedDate: '2021-03-29' diff --git a/microsite/data/plugins/badges.yaml b/microsite/data/plugins/badges.yaml index a5f397843d..86d16ab547 100644 --- a/microsite/data/plugins/badges.yaml +++ b/microsite/data/plugins/badges.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/backstage/community category: Discovery description: The badges plugin offers a set of badges that can be used outside of Backstage, showing information related to data from the catalog. documentation: https://github.com/backstage/backstage/blob/master/plugins/badges/README.md -iconUrl: img/badges.svg +iconUrl: /img/badges.svg npmPackageName: '@backstage/plugin-badges' addedDate: '2021-09-29' diff --git a/microsite/data/plugins/bazaar.yaml b/microsite/data/plugins/bazaar.yaml index c408229be2..ce3a4d5dfc 100644 --- a/microsite/data/plugins/bazaar.yaml +++ b/microsite/data/plugins/bazaar.yaml @@ -4,6 +4,6 @@ authorUrl: https://www.axis.com category: Discovery description: A marketplace where engineers can propose projects suitable for inner sourcing documentation: https://github.com/backstage/backstage/blob/master/plugins/bazaar/README.md -iconUrl: img/bazaar.svg +iconUrl: /img/bazaar.svg npmPackageName: '@backstage/plugin-bazaar' addedDate: '2022-01-11' diff --git a/microsite/data/plugins/catalog-graph.yaml b/microsite/data/plugins/catalog-graph.yaml index 6fd7214c4c..46ee7b2f17 100644 --- a/microsite/data/plugins/catalog-graph.yaml +++ b/microsite/data/plugins/catalog-graph.yaml @@ -5,6 +5,6 @@ authorUrl: https://sda.se/ category: Discovery description: Extend the Backstage Software Catalog with a graph that shows all entities and their relationships providing an easier way to discover the ecosystem. documentation: https://github.com/backstage/backstage/blob/master/plugins/catalog-graph/README.md -iconUrl: img/catalog-graph.svg +iconUrl: /img/catalog-graph.svg npmPackageName: '@backstage/plugin-catalog-graph' addedDate: '2021-09-15' diff --git a/microsite/data/plugins/cicd-statistics.yaml b/microsite/data/plugins/cicd-statistics.yaml index a43b56b0d9..ff5c2cccf3 100644 --- a/microsite/data/plugins/cicd-statistics.yaml +++ b/microsite/data/plugins/cicd-statistics.yaml @@ -5,7 +5,7 @@ authorUrl: https://github.com/spotify category: CI/CD description: Visualize CI/CD pipeline statistics such as build time or success and error rates. documentation: https://github.com/backstage/backstage/tree/master/plugins/cicd-statistics -iconUrl: img/cicd-statistics.svg +iconUrl: /img/cicd-statistics.svg npmPackageName: '@backstage/plugin-cicd-statistics' tags: - ci diff --git a/microsite/data/plugins/circleci.yaml b/microsite/data/plugins/circleci.yaml index 64582440ec..5d7978624b 100644 --- a/microsite/data/plugins/circleci.yaml +++ b/microsite/data/plugins/circleci.yaml @@ -5,7 +5,7 @@ authorUrl: https://github.com/spotify category: CI/CD description: Automate your development process with CI hosted in the cloud or on a private server. documentation: https://github.com/backstage/backstage/tree/master/plugins/circleci -iconUrl: img/circleci.png +iconUrl: /img/circleci.png npmPackageName: '@backstage/plugin-circleci' tags: - ci diff --git a/microsite/data/plugins/cloudsmith.yaml b/microsite/data/plugins/cloudsmith.yaml index 5fc417eec1..650f0b253c 100644 --- a/microsite/data/plugins/cloudsmith.yaml +++ b/microsite/data/plugins/cloudsmith.yaml @@ -3,11 +3,14 @@ title: Cloudsmith author: Roadie authorUrl: https://roadie.io category: CI/CD -description: Show Cloudsmith Repository stats on your backstage homepage. +description: Show Cloudsmith Repository stats, Quota, Audit logs and Security scans for specific repositories. documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/frontend/backstage-plugin-cloudsmith iconUrl: https://cloudsmith.com/img/cloudsmith-mini-dark.svg npmPackageName: '@roadiehq/backstage-plugin-cloudsmith' tags: - dashboards - monitoring + - audit logs + - security scanning + - usage addedDate: '2022-11-18' diff --git a/microsite/data/plugins/codescene.yaml b/microsite/data/plugins/codescene.yaml index bca23fc1fa..734e575282 100644 --- a/microsite/data/plugins/codescene.yaml +++ b/microsite/data/plugins/codescene.yaml @@ -5,6 +5,6 @@ authorUrl: https://codescene.com/ category: Quality description: CodeScene is a multi-purpose tool bridging code, business and people. See hidden risks and social patterns in your code. Prioritize and reduce technical debt. documentation: https://github.com/backstage/backstage/tree/master/plugins/codescene -iconUrl: img/codescene_logo.svg +iconUrl: /img/codescene_logo.svg npmPackageName: '@backstage/plugin-codescene' addedDate: '2022-04-12' diff --git a/microsite/data/plugins/cortex.yaml b/microsite/data/plugins/cortex.yaml index bf480ccfcd..77b01ddceb 100644 --- a/microsite/data/plugins/cortex.yaml +++ b/microsite/data/plugins/cortex.yaml @@ -5,7 +5,7 @@ authorUrl: https://www.getcortexapp.com category: Monitoring description: Grade the quality of your Backstage services using Scorecards. Automate production readiness, migrations, security audits, and more with CQL (Cortex Query Language). documentation: https://github.com/cortexapps/backstage-plugin -iconUrl: img/cortex.png +iconUrl: /img/cortex.png npmPackageName: '@cortexapps/backstage-plugin' tags: - web diff --git a/microsite/data/plugins/cost-insights.yaml b/microsite/data/plugins/cost-insights.yaml index 824769c174..603cd18416 100644 --- a/microsite/data/plugins/cost-insights.yaml +++ b/microsite/data/plugins/cost-insights.yaml @@ -5,7 +5,7 @@ authorUrl: https://github.com/spotify category: Discovery description: Visualize, understand and optimize your team's cloud costs. documentation: https://github.com/backstage/backstage/tree/master/plugins/cost-insights -iconUrl: img/cost-insights.png +iconUrl: /img/cost-insights.png npmPackageName: '@backstage/plugin-cost-insights' tags: - web diff --git a/microsite/data/plugins/dora-metrics.yaml b/microsite/data/plugins/dora-metrics.yaml index 65c2e4fe6e..3f59902f46 100644 --- a/microsite/data/plugins/dora-metrics.yaml +++ b/microsite/data/plugins/dora-metrics.yaml @@ -5,6 +5,6 @@ authorUrl: https://www.okayhq.com category: Metrics description: Embed dashboards (like DORA metrics) in your team or service pages from any dev tools (including Github, Gitlab, Jira, Argo, CircleCI, Buildkite, Pagerduty, Rollbar, Sentry, etc). documentation: https://github.com/OkayHQ/backstage-plugin -iconUrl: img/okay.png +iconUrl: /img/okay.png npmPackageName: '@okayhq/backstage-plugin' addedDate: '2022-06-16' diff --git a/microsite/data/plugins/dynatrace.yaml b/microsite/data/plugins/dynatrace.yaml index b68d993e4b..9ec1fee7b3 100644 --- a/microsite/data/plugins/dynatrace.yaml +++ b/microsite/data/plugins/dynatrace.yaml @@ -5,7 +5,7 @@ authorUrl: https://github.com/telus category: Monitoring description: View monitoring info from Dynatrace for services in your software catalog. documentation: https://github.com/backstage/backstage/tree/master/plugins/dynatrace -iconUrl: img/dynatrace.svg +iconUrl: /img/dynatrace.svg npmPackageName: '@backstage/plugin-dynatrace' tags: - dynatrace diff --git a/microsite/data/plugins/entity-feedback.yaml b/microsite/data/plugins/entity-feedback.yaml new file mode 100644 index 0000000000..3630eddb73 --- /dev/null +++ b/microsite/data/plugins/entity-feedback.yaml @@ -0,0 +1,10 @@ +--- +title: Entity Feedback +author: Phil Kuang +authorUrl: https://github.com/kuangp +category: Quality +description: Give and view feedback on entities available in the Backstage catalog. +documentation: https://github.com/backstage/backstage/tree/master/plugins/entity-feedback +iconUrl: /img/entity-feedback-logo.png +npmPackageName: '@backstage/plugin-entity-feedback' +addedDate: '2023-01-20' diff --git a/microsite/data/plugins/git-release-manager.yaml b/microsite/data/plugins/git-release-manager.yaml index e7000be694..6d357a23f6 100644 --- a/microsite/data/plugins/git-release-manager.yaml +++ b/microsite/data/plugins/git-release-manager.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/spotify category: Release management description: Manage releases without having to juggle git commands. documentation: https://github.com/backstage/backstage/tree/master/plugins/git-release-manager -iconUrl: img/git-release-manager-logo.svg +iconUrl: /img/git-release-manager-logo.svg npmPackageName: '@backstage/plugin-git-release-manager' addedDate: '2021-10-04' diff --git a/microsite/data/plugins/github-pull-requests-board.yaml b/microsite/data/plugins/github-pull-requests-board.yaml index 0c3435b581..09776eb468 100644 --- a/microsite/data/plugins/github-pull-requests-board.yaml +++ b/microsite/data/plugins/github-pull-requests-board.yaml @@ -5,6 +5,6 @@ authorUrl: https://engineering.dazn.com category: Source Control Mgmt description: View all open GitHub pull requests owned by your team in Backstage. documentation: https://github.com/backstage/backstage/tree/master/plugins/github-pull-requests-board -iconUrl: img/github-pull-requests-board-logo.svg +iconUrl: /img/github-pull-requests-board-logo.svg npmPackageName: '@backstage/plugin-github-pull-requests-board' addedDate: '2022-05-10' diff --git a/microsite/data/plugins/graphql-voyager.yaml b/microsite/data/plugins/graphql-voyager.yaml new file mode 100644 index 0000000000..c6ebf5e412 --- /dev/null +++ b/microsite/data/plugins/graphql-voyager.yaml @@ -0,0 +1,13 @@ +--- +title: GraphQL Voyager +author: PostNL +authorUrl: https://github.com/postnl +category: Debugging +description: Integrates the GraphQL Voyager tool inside Backstage as a plugin. +documentation: https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager +iconUrl: https://res.cloudinary.com/apideck/image/upload/v1612724234/icons/graphql-voyager.png +npmPackageName: '@backstage/plugin-graphql-voyager' +tags: + - graphql + - graphql-voyager +addedDate: '2023-01-27' diff --git a/microsite/data/plugins/home.yaml b/microsite/data/plugins/home.yaml index cf67f6b43f..0ca2de56f2 100644 --- a/microsite/data/plugins/home.yaml +++ b/microsite/data/plugins/home.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/spotify category: Discovery description: This plugin provides a composable home page, and ability to create home page components documentation: https://github.com/backstage/backstage/blob/master/plugins/home/README.md -iconUrl: img/home.png +iconUrl: /img/home.png npmPackageName: '@backstage/plugin-home' addedDate: '2021-08-31' diff --git a/microsite/data/plugins/humanitec.yaml b/microsite/data/plugins/humanitec.yaml index 1b79270582..7265b9d917 100644 --- a/microsite/data/plugins/humanitec.yaml +++ b/microsite/data/plugins/humanitec.yaml @@ -4,9 +4,9 @@ author: Frontside authorUrl: 'https://frontside.com' category: Deployment # A single category e.g. CI, Machine Learning, Services, Monitoring description: | - Show workloads, environments and resources deployed by Humanitec Platform Orchestrator. + Show workloads, environments and resources deployed by Humanitec Platform Orchestrator. Plugin includes an Entity ComponentCard, Backend API route and scaffolder actions. documentation: https://github.com/thefrontside/playhouse/tree/main/plugins/humanitec -iconUrl: img/humanitec-logo.png +iconUrl: /img/humanitec-logo.png npmPackageName: '@frontside/backstage-plugin-humanitec' addedDate: '2022-06-22' diff --git a/microsite/data/plugins/kubevela.yaml b/microsite/data/plugins/kubevela.yaml new file mode 100644 index 0000000000..524a046025 --- /dev/null +++ b/microsite/data/plugins/kubevela.yaml @@ -0,0 +1,15 @@ +--- +title: KubeVela +author: Charlie Chiang +authorUrl: https://github.com/charlie0129 +category: App Delivery +description: KubeVela is a modern software delivery platform that makes deploying and operating applications across today's hybrid, multi-cloud environments easier, faster and more reliable. +documentation: https://github.com/kubevela-contrib/backstage-plugin-kubevela +iconUrl: https://kubevela.io/img/logo.svg +npmPackageName: '@oamdev/plugin-kubevela-backend' +tags: + - cd + - delivery + - multi-cluster + - deployment as code +addedDate: '2023-02-12' diff --git a/microsite/data/plugins/octopus-deploy.yaml b/microsite/data/plugins/octopus-deploy.yaml new file mode 100644 index 0000000000..1ed7439695 --- /dev/null +++ b/microsite/data/plugins/octopus-deploy.yaml @@ -0,0 +1,10 @@ +--- +title: 'Octopus Deploy' +author: jmezach +authorUrl: https://jmezach.github.io/ +category: CI/CD +description: Easily view your Octopus Deploy releases within the Software Catalog +documentation: https://github.com/backstage/backstage/blob/master/plugins/octopus-deploy/README.md +iconUrl: /img/octopus-deploy.svg +npmPackageName: '@backstage/plugin-octopus-deploy' +addedDate: '2023-02-24' diff --git a/microsite/data/plugins/playlist.yaml b/microsite/data/plugins/playlist.yaml index 7d9c13082d..4e7f442e03 100644 --- a/microsite/data/plugins/playlist.yaml +++ b/microsite/data/plugins/playlist.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/kuangp category: Discovery description: Create, share, and follow custom collections of entities available in the Backstage catalog. documentation: https://github.com/backstage/backstage/tree/master/plugins/playlist -iconUrl: img/playlist-logo.png +iconUrl: /img/playlist-logo.png npmPackageName: '@backstage/plugin-playlist' addedDate: '2022-07-02' diff --git a/microsite/data/plugins/qeta.yaml b/microsite/data/plugins/qeta.yaml index a4a3c343a4..0b9dc21cfd 100644 --- a/microsite/data/plugins/qeta.yaml +++ b/microsite/data/plugins/qeta.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/drodil category: Services description: Ask and answer questions within Backstage documentation: https://github.com/drodil/backstage-plugin-qeta -iconUrl: img/qeta-logo.png +iconUrl: /img/qeta-logo.png npmPackageName: '@drodil/backstage-plugin-qeta' addedDate: '2022-12-21' diff --git a/microsite/data/plugins/s3-viewer.yaml b/microsite/data/plugins/s3-viewer.yaml new file mode 100644 index 0000000000..2eeeb23415 --- /dev/null +++ b/microsite/data/plugins/s3-viewer.yaml @@ -0,0 +1,12 @@ +--- +title: S3 Viewer +author: Spreadgroup +authorUrl: https://github.com/spreadshirt +category: AWS +description: Visualization of S3 buckets and their contents in file explorer style. +documentation: https://github.com/spreadshirt/backstage-plugin-s3/ +iconUrl: /img/s3-bucket.svg +npmPackageName: '@spreadshirt/backstage-plugin-s3-viewer' +tags: + - s3 +addedDate: '2023-01-18' diff --git a/microsite/data/plugins/scaffolder-backend-dotnet.yaml b/microsite/data/plugins/scaffolder-backend-dotnet.yaml index 2b032621c9..03fee737cb 100644 --- a/microsite/data/plugins/scaffolder-backend-dotnet.yaml +++ b/microsite/data/plugins/scaffolder-backend-dotnet.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/alefcarlos category: Scaffolder description: Here you can find all .NET related actions to improve your scaffolder. documentation: https://github.com/alefcarlos/plusultra-dotnet-backstage-plugins/blob/main/plugins/scaffolder-dotnet-backend/README.md -iconUrl: img/scaffolder-backend-dotnet-icon.png +iconUrl: /img/scaffolder-backend-dotnet-icon.png npmPackageName: '@plusultra/plugin-scaffolder-dotnet-backend' addedDate: '2022-01-24' diff --git a/microsite/data/plugins/scaffolder-backend-module-rails.yaml b/microsite/data/plugins/scaffolder-backend-module-rails.yaml index 4110dcbed0..f740b95738 100644 --- a/microsite/data/plugins/scaffolder-backend-module-rails.yaml +++ b/microsite/data/plugins/scaffolder-backend-module-rails.yaml @@ -5,6 +5,6 @@ authorUrl: https://angeliski.com.br/ category: Scaffolder description: Here you can find all Rails related features to improve your scaffolder. documentation: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-rails/README.md -iconUrl: img/rails-icon.png +iconUrl: /img/rails-icon.png npmPackageName: '@backstage/plugin-scaffolder-backend-module-rails' addedDate: '2021-06-24' diff --git a/microsite/data/plugins/scaffolder-backend-roadie-http-request.yaml b/microsite/data/plugins/scaffolder-backend-roadie-http-request.yaml index 254bebe8e7..e377f4d1f2 100644 --- a/microsite/data/plugins/scaffolder-backend-roadie-http-request.yaml +++ b/microsite/data/plugins/scaffolder-backend-roadie-http-request.yaml @@ -5,6 +5,6 @@ authorUrl: https://roadie.io/?utm_source=backstage.io&utm_medium=marketplace&utm category: Scaffolder description: An action to fire an arbitrary HTTP request documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/plugins/scaffolder-actions/scaffolder-backend-module-http-request/README.md -iconUrl: img/scaffolder-http-request-logo.svg +iconUrl: /img/scaffolder-http-request-logo.svg npmPackageName: '@roadiehq/scaffolder-backend-module-http-request' addedDate: '2022-03-03' diff --git a/microsite/data/plugins/scaffolder-backend-roadie-utils.yaml b/microsite/data/plugins/scaffolder-backend-roadie-utils.yaml index ef3814f8ea..dace41994f 100644 --- a/microsite/data/plugins/scaffolder-backend-roadie-utils.yaml +++ b/microsite/data/plugins/scaffolder-backend-roadie-utils.yaml @@ -5,6 +5,6 @@ authorUrl: https://roadie.io/?utm_source=backstage.io&utm_medium=marketplace&utm category: Scaffolder description: A collection of utility actions including sleep, zip and file manipulation. documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/plugins/scaffolder-actions/scaffolder-backend-module-utils/README.md -iconUrl: img/scaffolder-utils-logo.png +iconUrl: /img/scaffolder-utils-logo.png npmPackageName: '@roadiehq/scaffolder-backend-module-utils' addedDate: '2022-03-03' diff --git a/microsite/data/plugins/score-card.yaml b/microsite/data/plugins/score-card.yaml index c647f8dd15..48f619af2a 100644 --- a/microsite/data/plugins/score-card.yaml +++ b/microsite/data/plugins/score-card.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/Oriflame category: Quality description: Visualization of maturity (and improving it) of services/systems thanks to a review process. documentation: https://github.com/Oriflame/backstage-plugins/tree/main/plugins/score-card -iconUrl: img/score-card-plugin-logo.png +iconUrl: /img/score-card-plugin-logo.png npmPackageName: '@oriflame/backstage-plugin-score-card' addedDate: '2022-10-06' diff --git a/microsite/data/plugins/shortcuts.yaml b/microsite/data/plugins/shortcuts.yaml index d7c63ba666..7c3f2c6d56 100644 --- a/microsite/data/plugins/shortcuts.yaml +++ b/microsite/data/plugins/shortcuts.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/spotify category: Utility description: The shortcuts plugin allows a user to have easy access to pages within a Backstage app by storing them as "shortcuts" in the Sidebar. documentation: https://github.com/backstage/backstage/blob/master/plugins/shortcuts/README.md -iconUrl: img/shortcuts.svg +iconUrl: /img/shortcuts.svg npmPackageName: '@backstage/plugin-shortcuts' addedDate: '2021-10-06' diff --git a/microsite/data/plugins/sonarqube.yaml b/microsite/data/plugins/sonarqube.yaml index 2eab08efff..f24441c46e 100644 --- a/microsite/data/plugins/sonarqube.yaml +++ b/microsite/data/plugins/sonarqube.yaml @@ -5,6 +5,6 @@ authorUrl: https://sda.se/ category: Quality description: Components to display code quality metrics from SonarCloud and SonarQube. documentation: https://github.com/backstage/backstage/blob/master/plugins/sonarqube/README.md -iconUrl: img/sonarqube-icon.svg +iconUrl: /img/sonarqube-icon.svg npmPackageName: '@backstage/plugin-sonarqube' addedDate: '2020-11-03' diff --git a/microsite/data/plugins/splunk-on-call.yaml b/microsite/data/plugins/splunk-on-call.yaml index 7a29ab3bf9..b9021a337d 100644 --- a/microsite/data/plugins/splunk-on-call.yaml +++ b/microsite/data/plugins/splunk-on-call.yaml @@ -5,7 +5,7 @@ authorUrl: https://github.com/ayshiff category: Monitoring description: Splunk On-Call offers a simple way to identify incidents and escalation policies. documentation: https://github.com/backstage/backstage/tree/master/plugins/splunk-on-call -iconUrl: img/splunk-black-white-bg.png +iconUrl: /img/splunk-black-white-bg.png npmPackageName: '@backstage/plugin-splunk-on-call' tags: - monitoring diff --git a/microsite/data/plugins/stack-overflow.yaml b/microsite/data/plugins/stack-overflow.yaml index 27a3a8cf97..a5e2122f8a 100644 --- a/microsite/data/plugins/stack-overflow.yaml +++ b/microsite/data/plugins/stack-overflow.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/spotify category: Discovery description: Provides Stack Overflow specific functionality that can be used in different ways (e.g. for homepage and search) to compose your Backstage App. documentation: https://github.com/backstage/backstage/blob/master/plugins/stack-overflow -iconUrl: img/stack-overflow-logo.svg +iconUrl: /img/stack-overflow-logo.svg npmPackageName: '@backstage/plugin-stack-overflow' addedDate: '2022-06-14' diff --git a/microsite/data/plugins/stackstorm.yaml b/microsite/data/plugins/stackstorm.yaml new file mode 100644 index 0000000000..ade6ca860f --- /dev/null +++ b/microsite/data/plugins/stackstorm.yaml @@ -0,0 +1,14 @@ +--- +title: StackStorm +author: ExpediaGroup +authorUrl: https://github.com/ExpediaGroup +category: Automation +description: Manage StackStorm workflow executions from within Backstage. +documentation: https://github.com/backstage/backstage/tree/master/plugins/stackstorm +iconUrl: /img/stackstorm.png +npmPackageName: '@backstage/plugin-stackstorm' +tags: + - stackstorm + - st2 + - automation + - workflow diff --git a/microsite/data/plugins/toolbox.yaml b/microsite/data/plugins/toolbox.yaml index 9d9adf0224..b488b24369 100644 --- a/microsite/data/plugins/toolbox.yaml +++ b/microsite/data/plugins/toolbox.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/drodil category: Tools description: Development related tools within Backstage documentation: https://github.com/drodil/backstage-plugin-toolbox -iconUrl: img/toolbox-logo.png +iconUrl: /img/toolbox-logo.png npmPackageName: '@drodil/backstage-plugin-toolbox' addedDate: '2023-01-09' diff --git a/microsite/data/plugins/torque.yaml b/microsite/data/plugins/torque.yaml new file mode 100644 index 0000000000..1480c3bad9 --- /dev/null +++ b/microsite/data/plugins/torque.yaml @@ -0,0 +1,12 @@ +--- +title: Torque +author: Quali +authorUrl: 'https://www.qtorque.io/' +category: Orchestration +description: | + Deploy the environments you need to keep building, testing and delivering incredible code. + Plugin includes an Entity ComponentCard, Backend API route and scaffolder actions. +documentation: https://github.com/QualiTorque/torque-backstage-plugin/tree/main/packages/torque#readme +iconUrl: https://user-images.githubusercontent.com/8643801/214640977-751bc338-6b77-40a0-a897-cba41b6f006a.png +npmPackageName: '@qtorque/backstage-plugin-torque' +addedDate: '2023-01-25' diff --git a/microsite/data/plugins/vault.yaml b/microsite/data/plugins/vault.yaml index 822f5dcc90..29e20417d8 100644 --- a/microsite/data/plugins/vault.yaml +++ b/microsite/data/plugins/vault.yaml @@ -3,9 +3,9 @@ title: Vault author: Spread Group authorUrl: https://github.com/ivangonzalezacuna category: Vault -description: Visualize a list secrets stored in your vault instance. +description: Visualize a list of the secrets stored in your HashiCorp Vault instance. documentation: https://github.com/backstage/backstage/tree/master/plugins/vault -iconUrl: img/vault.png +iconUrl: /img/vault.png npmPackageName: '@backstage/plugin-vault' tags: - vault diff --git a/microsite/data/plugins/xcmetrics.yaml b/microsite/data/plugins/xcmetrics.yaml index 91db85ebaa..f02b195eb5 100644 --- a/microsite/data/plugins/xcmetrics.yaml +++ b/microsite/data/plugins/xcmetrics.yaml @@ -5,6 +5,6 @@ authorUrl: https://github.com/spotify category: Monitoring description: Discover valuable insights hiding inside Xcode’s build logs. documentation: https://xcmetrics.io/ -iconUrl: img/xcmetrics-icon.png +iconUrl: /img/xcmetrics-icon.png npmPackageName: '@backstage/plugin-xcmetrics' addedDate: '2021-08-06' diff --git a/microsite/pages/en/community.js b/microsite/pages/en/community.js index a5fd5353e0..69330e6569 100644 --- a/microsite/pages/en/community.js +++ b/microsite/pages/en/community.js @@ -62,7 +62,9 @@ const Background = props => { <li> <Block.Paragraph style={{ marginBottom: '0' }}> Chat and get support on our{' '} - <a href="https://discord.gg/MUpMjP2">Discord</a> + <a href="https://discord.gg/backstage-687207715902193673"> + Discord + </a> </Block.Paragraph> </li> <li> diff --git a/microsite/pages/en/live.js b/microsite/pages/en/live.js index e2f3267b62..cad5e52bd2 100644 --- a/microsite/pages/en/live.js +++ b/microsite/pages/en/live.js @@ -34,7 +34,7 @@ const Background = props => { <Block.Container style={{ justifyContent: 'flex-start' }}> <Block.MediaFrame> <iframe - src="https://www.youtube.com/embed/ZAMwTDj_m88" + src="https://www.youtube.com/embed/wuztcqiQIa0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" @@ -45,7 +45,7 @@ const Background = props => { <iframe width="300" height="500" - src="https://www.youtube.com/live_chat?v=ZAMwTDj_m88&embed_domain=backstage.io&dark_theme=1" + src="https://www.youtube.com/live_chat?v=wuztcqiQIa0&embed_domain=backstage.io&dark_theme=1" ></iframe> </Block.MediaFrame> </Block.Container> @@ -58,7 +58,9 @@ const Background = props => { <Block.Paragraph> Main community channels <br />- Chat and get support on our{' '} - <a href="https://discord.gg/MUpMjP2">Discord</a> + <a href="https://discord.gg/backstage-687207715902193673"> + Discord + </a> <br />- Get into contributing with the{' '} <a href="https://github.com/backstage/backstage/contribute"> Good First Issues diff --git a/microsite/pages/en/on-demand.js b/microsite/pages/en/on-demand.js index 41a730205b..15059e91a1 100644 --- a/microsite/pages/en/on-demand.js +++ b/microsite/pages/en/on-demand.js @@ -20,7 +20,7 @@ const ondemandMetadata = fs .reverse() .map(file => yaml.load(fs.readFileSync(`./data/on-demand/${file}`, 'utf8'))); const truncate = text => - text.length > 170 ? text.substr(0, 170) + '...' : text; + text.length > 170 ? text.slice(0, 170) + '...' : text; const addVideoDocsLink = '/docs/overview/support'; const defaultIconUrl = 'img/logo-gradient-on-dark.svg'; diff --git a/microsite/pages/en/plugins.js b/microsite/pages/en/plugins.js index 9a0a59bd4b..237161b98a 100644 --- a/microsite/pages/en/plugins.js +++ b/microsite/pages/en/plugins.js @@ -19,7 +19,7 @@ const pluginMetadata = fs .map(file => yaml.load(fs.readFileSync(`./data/plugins/${file}`, 'utf8'))) .sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); const truncate = text => - text.length > 170 ? text.substr(0, 170) + '...' : text; + text.length > 170 ? text.slice(0, 170) + '...' : text; const newForDays = 100; diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 76231b0999..adeb5a6609 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -1,6 +1,7 @@ { "releases": { "Release Notes": [ + "releases/v1.11.0", "releases/v1.10.0", "releases/v1.9.0", "releases/v1.8.0", @@ -87,7 +88,8 @@ "features/kubernetes/installation", "features/kubernetes/configuration", "features/kubernetes/authentication", - "features/kubernetes/troubleshooting" + "features/kubernetes/troubleshooting", + "features/kubernetes/proxy" ] }, { @@ -103,6 +105,7 @@ "features/software-templates/writing-custom-actions", "features/software-templates/writing-custom-field-extensions", "features/software-templates/writing-custom-step-layouts", + "features/software-templates/testing-scaffolder-alpha", "features/software-templates/migrating-from-v1beta2-to-v1beta3" ] }, @@ -201,7 +204,8 @@ "label": "GitLab", "ids": [ "integrations/gitlab/locations", - "integrations/gitlab/discovery" + "integrations/gitlab/discovery", + "integrations/gitlab/org" ] }, { @@ -364,43 +368,40 @@ ], "FAQ": ["FAQ"], "Experimental Backend System": [ + "backend-system/index", { - "_hidden_": [ - "backend-system/index", - { - "type": "subcategory", - "label": "Architecture", - "ids": [ - "backend-system/architecture/index", - "backend-system/architecture/services", - "backend-system/architecture/plugins", - "backend-system/architecture/extension-points", - "backend-system/architecture/modules" - ] - }, - { - "type": "subcategory", - "label": "Building Backends", - "ids": [ - "backend-system/building-backends/index", - "backend-system/building-backends/migrating" - ] - }, - { - "type": "subcategory", - "label": "Building Plugins & Modules", - "ids": [ - "backend-system/building-plugins-and-modules/index", - "backend-system/building-plugins-and-modules/testing", - "backend-system/building-plugins-and-modules/migrating" - ] - }, - { - "type": "subcategory", - "label": "Core Services", - "ids": ["backend-system/core-services/index"] - } + "type": "subcategory", + "label": "Architecture", + "ids": [ + "backend-system/architecture/index", + "backend-system/architecture/services", + "backend-system/architecture/plugins", + "backend-system/architecture/extension-points", + "backend-system/architecture/modules", + "backend-system/architecture/naming-patterns" ] + }, + { + "type": "subcategory", + "label": "Building Backends", + "ids": [ + "backend-system/building-backends/index", + "backend-system/building-backends/migrating" + ] + }, + { + "type": "subcategory", + "label": "Building Plugins & Modules", + "ids": [ + "backend-system/building-plugins-and-modules/index", + "backend-system/building-plugins-and-modules/testing", + "backend-system/building-plugins-and-modules/migrating" + ] + }, + { + "type": "subcategory", + "label": "Core Services", + "ids": ["backend-system/core-services/index"] } ] } diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js index 68226b5681..5329ebb688 100644 --- a/microsite/siteConfig.js +++ b/microsite/siteConfig.js @@ -138,8 +138,9 @@ const siteConfig = { ], algolia: { - apiKey: '8d115c9875ba0f4feaee95bab55a1645', - indexName: 'backstage', + apiKey: '10b9e6c92a4513c1cf390f50e53aad1f', + indexName: 'backstage_io', + appId: 'JCMFNHCHI8', searchParameters: {}, // Optional (if provided by Algolia) }, }; diff --git a/microsite/static/.nojekyll b/microsite/static/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/microsite/static/CNAME b/microsite/static/CNAME new file mode 100644 index 0000000000..c3be0f8d9b --- /dev/null +++ b/microsite/static/CNAME @@ -0,0 +1 @@ +backstage.io \ No newline at end of file diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index 59f9571a70..a2a63e2473 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -1244,12 +1244,24 @@ h3.collapsible span.arrow { transform: translateY(-50%) translateX(500px); } +@media only screen and (max-height: 675px) { + .Sidebar__Container { + top: 65%; + } +} + +@media only screen and (max-width: 650px) { + .Sidebar__Container { + transform: translateY(-50%) translateX(300px); + } +} + .Sidebar__Container--open { transform: translateY(-50%); } .Sidebar__Button { - transform: rotate(-90deg) translateY(48px); + transform: rotate(-90deg) translateY(49px); padding: 12px 16px; border-radius: 8px 8px 0 0; background-color: rgb(92, 214, 200); @@ -1263,18 +1275,34 @@ h3.collapsible span.arrow { cursor: pointer; } +@media only screen and (max-width: 650px) { + .Sidebar__Button { + width: 175px; + font-size: 15px; + } +} + .Sidebar__Button:hover { - transform: rotate(-90deg) translateY(48px) scale(1.01); + transform: rotate(-90deg) translateY(49px) scale(1.01); } #Sidebar__HubSpotContainer { width: 500px; background-color: white; border-radius: 8px 0 0 8px; - padding: 16px; - padding-bottom: 0px; - z-index: 10001; - min-height: 260px; + height: 500px; +} + +@media only screen and (max-width: 650px) { + #Sidebar__HubSpotContainer { + width: 300px; + } +} + +#Sidebar__HubSpotContainer form { + overflow-y: auto; + height: 100%; + padding: 25px 25px 0px 25px; } #Sidebar__HubSpotContainer .hs-button { @@ -1284,6 +1312,7 @@ h3.collapsible span.arrow { color: rgb(0, 0, 0); border: 0; } + #Sidebar__HubSpotContainer.submitted-message { min-height: 260px; display: flex; diff --git a/microsite/static/img/entity-feedback-logo.png b/microsite/static/img/entity-feedback-logo.png new file mode 100644 index 0000000000..e172b260a0 Binary files /dev/null and b/microsite/static/img/entity-feedback-logo.png differ diff --git a/microsite/static/img/octopus-deploy.svg b/microsite/static/img/octopus-deploy.svg new file mode 100644 index 0000000000..a3f7927617 --- /dev/null +++ b/microsite/static/img/octopus-deploy.svg @@ -0,0 +1 @@ +<svg id="Logos" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 140"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M19.13 103.13c9.35-6 20.33-16.34 16.16-28.5C33 68 29.87 62.33 29.45 55.17a43.84 43.84 0 0 1 2.66-17.83C40.49 14.85 65.4 4.2 87.92 11c20.85 6.26 35.2 30.48 26.56 51.58-5 12.18-7.22 21.58 3.9 31.05 3 2.57 10.33 6.42 10.31 11 0 6-11.74-1.27-13-2.31 1.49 2.6 16.24 18 6.85 19.08-8.64 1-16.27-11.06-21.47-16.25-8.72-8.7-7.21 10.55-7.25 14.53-.06 6.28-4.5 19-12.46 10.72-6.58-6.84-4.09-17.76-8.66-25.36-5-8.33-13.34 8.33-15.42 11.37-2.33 3.4-14 19.89-18.59 11.1-3.76-7.13 2.25-18.3 5.2-24.75-1.08 2.34-8.71 5.8-10.94 6.93-5 2.54-10.15 4.05-15.82 3.65-11.73-.84-2.73-7.07 2-10.15z"/></svg> \ No newline at end of file diff --git a/microsite/static/img/s3-bucket.svg b/microsite/static/img/s3-bucket.svg new file mode 100644 index 0000000000..a92ac68982 --- /dev/null +++ b/microsite/static/img/s3-bucket.svg @@ -0,0 +1 @@ +<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 100 100" height="100px" version="1.1" viewBox="0 0 100 100" width="100px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"><g><g><g enable-background="new "><g enable-background="new "><defs><path d="M77.586,28.061v2.772c0,3.559-12.344,6.433-27.578,6.433 c-15.25,0-27.594-2.874-27.594-6.433v-2.772c0,3.559,12.344,6.432,27.594,6.432C65.242,34.492,77.586,31.619,77.586,28.061z" enable-background="new " id="SVGID_5_"/></defs><clipPath id="SVGID_2_"/><g clip-path="url(#SVGID_2_)" enable-background="new "><path d="M77.586,28.061v2.772c0,0.084-0.006,0.168-0.02,0.25v-2.771C77.58,28.228,77.586,28.145,77.586,28.061 " fill="#8C3323"/><path d="M77.566,28.312v2.771c-0.018,0.098-0.039,0.193-0.074,0.289v-2.773 C77.527,28.503,77.549,28.407,77.566,28.312" fill="#8C3323"/><path d="M77.492,28.599v2.773c-0.049,0.135-0.113,0.268-0.195,0.4v-2.773 C77.379,28.866,77.443,28.734,77.492,28.599" fill="#8C3323"/><path d="M77.297,28.999v2.773c-1.945,3.111-13.42,5.493-27.289,5.493c-15.25,0-27.594-2.874-27.594-6.433 v-2.772c0,3.559,12.344,6.432,27.594,6.432C63.877,34.492,75.352,32.11,77.297,28.999" fill="#8C3323"/></g></g><g><path d="M50.008,21.628c15.234,0,27.578,2.875,27.578,6.433c0,3.559-12.344,6.432-27.578,6.432 c-15.25,0-27.594-2.873-27.594-6.432C22.414,24.503,34.758,21.628,50.008,21.628z" fill="#E15343"/></g><g><polygon fill="#8C3323" points="77.428,33.006 77.428,35.779 68.387,74.069 67.67,72.257 "/></g><g><polygon fill="#8C3323" points="31.613,71.296 31.613,74.069 22.572,35.779 22.572,33.006 "/></g><g enable-background="new "><defs><path d="M68.387,71.296v2.773c0,2.387-8.217,4.303-18.379,4.303 s-18.395-1.916-18.395-4.303v-2.773c0,2.389,8.232,4.303,18.395,4.303S68.387,73.685,68.387,71.296z" enable-background="new " id="SVGID_7_"/></defs><clipPath id="SVGID_4_"/><g clip-path="url(#SVGID_4_)" enable-background="new "><path d="M68.387,71.296v2.773c0,0.057-0.004,0.112-0.012,0.168v-2.771 C68.383,71.409,68.387,71.353,68.387,71.296" fill="#8C3323"/><path d="M68.375,71.466v2.771c-0.012,0.066-0.027,0.131-0.051,0.196v-2.772 C68.348,71.596,68.363,71.53,68.375,71.466" fill="#8C3323"/><path d="M68.324,71.661v2.772c-0.033,0.091-0.076,0.18-0.131,0.268v-2.772 C68.248,71.839,68.291,71.751,68.324,71.661" fill="#8C3323"/><path d="M68.67,71.007l-0.477,3.694c-1.301,2.084-8.939,3.671-18.186,3.671 c-10.162,0-18.395-1.916-18.395-4.303v-2.773c0,2.389,8.232,4.303,18.395,4.303c9.246,0,16.885-1.585,18.186-3.67" fill="#8C3323"/></g></g><g><path d="M50.008,38.752c14.24,0,25.951-2.515,27.42-5.746l-9.041,38.29c0,2.389-8.217,4.303-18.379,4.303 s-18.395-1.914-18.395-4.303l-9.041-38.29C24.043,36.237,35.77,38.752,50.008,38.752z" fill="#E15343"/></g></g></g><rect clip-rule="evenodd" fill="#FFFFFF" fill-rule="evenodd" height="11.398" width="11.477" x="43.762" y="42.854"/><ellipse clip-rule="evenodd" cx="40.287" cy="63.9" fill="#FFFFFF" fill-rule="evenodd" rx="5.658" ry="5.62"/><polygon clip-rule="evenodd" fill="#FFFFFF" fill-rule="evenodd" points="64.09,69.257 52.748,69.257 58.42,58.784 "/></g></g></svg> \ No newline at end of file diff --git a/microsite/static/img/stackstorm.png b/microsite/static/img/stackstorm.png new file mode 100644 index 0000000000..f017906e07 Binary files /dev/null and b/microsite/static/img/stackstorm.png differ diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 3bc81619e3..95b0c2885b 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -7369,11 +7369,11 @@ __metadata: linkType: hard "prettier@npm:^2.6.2": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" + version: 2.8.4 + resolution: "prettier@npm:2.8.4" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e languageName: node linkType: hard diff --git a/mkdocs.yml b/mkdocs.yml index 38369f4748..4b5c2cf7a2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -154,6 +154,7 @@ nav: - Included providers: - Auth0: 'auth/auth0/provider.md' - Azure: 'auth/microsoft/provider.md' + - Azure EasyAuth: 'auth/microsoft/azure-easyauth.md' - GitHub: 'auth/github/provider.md' - GitLab: 'auth/gitlab/provider.md' - Google: 'auth/google/provider.md' diff --git a/package.json b/package.json index 7e6a66e892..9a60fb9d4a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:backend": "yarn workspace backend build", "build:all": "backstage-cli repo build --all", "build:api-reports": "yarn build:api-reports:only --tsc", - "build:api-reports:only": "backstage-repo-tools api-reports --allow-warnings 'packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-wrong-input-file-type", + "build:api-reports:only": "backstage-repo-tools api-reports --allow-warnings 'packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-wrong-input-file-type --validate-release-tags", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs", "tsc": "tsc", "tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false", @@ -27,6 +27,7 @@ "create-plugin": "echo \"use 'yarn new' instead\"", "release": "node scripts/prepare-release.js && changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' '.changeset/*.json' && yarn install --no-immutable", "prettier:check": "prettier --check .", + "prettier:fix": "prettier --write .", "storybook": "yarn ./storybook run start", "snyk:test": "npx snyk test --yarn-workspaces --strict-out-of-sync=false", "snyk:test:package": "yarn snyk:test --include", @@ -46,7 +47,7 @@ "@types/react": "^17", "@types/react-dom": "^17" }, - "version": "1.10.0", + "version": "1.12.0-next.0", "dependencies": { "@backstage/errors": "workspace:^", "@manypkg/get-packages": "^1.1.3" diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 9a492ecaf7..fe9c1ac9ba 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/app-defaults +## 1.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-permission-react@0.4.11-next.0 + +## 1.2.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-permission-react@0.4.10 + +## 1.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.9 + +## 1.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.9 + ## 1.1.0 ### Minor Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index edfad83e78..b9116c21ae 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "1.1.0", + "version": "1.2.1-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index b3a3d55736..f8464adaff 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -25,6 +25,7 @@ import { GitlabAuth, MicrosoftAuth, BitbucketAuth, + BitbucketServerAuth, OAuthRequestManager, WebStorage, UrlPatternDiscovery, @@ -53,6 +54,7 @@ import { configApiRef, oneloginAuthApiRef, bitbucketAuthApiRef, + bitbucketServerAuthApiRef, atlassianAuthApiRef, } from '@backstage/core-plugin-api'; import { @@ -226,6 +228,19 @@ export const apis = [ environment: configApi.getOptionalString('auth.environment'), }), }), + createApiFactory({ + api: bitbucketServerAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + BitbucketServerAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['REPO_READ'], + }), + }), createApiFactory({ api: atlassianAuthApiRef, deps: { diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 377507d639..78633bc1d9 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,325 @@ # example-app +## 0.2.81-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.1-next.0 + - @backstage/cli@0.22.4-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/plugin-techdocs@1.6.0-next.0 + - @backstage/plugin-api-docs@0.9.1-next.0 + - @backstage/plugin-org@0.6.6-next.0 + - @backstage/plugin-stackstorm@0.1.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/plugin-cost-insights@0.12.5-next.0 + - @backstage/plugin-scaffolder@1.11.1-next.0 + - @backstage/plugin-jenkins@0.7.15-next.0 + - @backstage/plugin-playlist@0.1.7-next.0 + - @backstage/plugin-explore@0.4.1-next.0 + - @backstage/plugin-catalog-graph@0.2.28-next.0 + - @backstage/plugin-catalog-import@0.9.6-next.0 + - @backstage/app-defaults@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-airbrake@0.3.16-next.0 + - @backstage/plugin-apache-airflow@0.2.9-next.0 + - @backstage/plugin-azure-devops@0.2.7-next.0 + - @backstage/plugin-azure-sites@0.1.5-next.0 + - @backstage/plugin-badges@0.2.40-next.0 + - @backstage/plugin-circleci@0.3.16-next.0 + - @backstage/plugin-cloudbuild@0.3.16-next.0 + - @backstage/plugin-code-coverage@0.2.9-next.0 + - @backstage/plugin-dynatrace@3.0.0-next.0 + - @backstage/plugin-entity-feedback@0.1.1-next.0 + - @backstage/plugin-gcalendar@0.3.12-next.0 + - @backstage/plugin-gcp-projects@0.3.35-next.0 + - @backstage/plugin-github-actions@0.5.16-next.0 + - @backstage/plugin-gocd@0.1.22-next.0 + - @backstage/plugin-graphiql@0.2.48-next.0 + - @backstage/plugin-home@0.4.32-next.0 + - @backstage/plugin-kafka@0.3.16-next.0 + - @backstage/plugin-kubernetes@0.7.9-next.0 + - @backstage/plugin-lighthouse@0.4.1-next.0 + - @backstage/plugin-linguist@0.1.1-next.0 + - @backstage/plugin-linguist-common@0.1.0 + - @backstage/plugin-microsoft-calendar@0.1.1-next.0 + - @backstage/plugin-newrelic@0.3.34-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.9-next.0 + - @backstage/plugin-pagerduty@0.5.9-next.0 + - @backstage/plugin-permission-react@0.4.11-next.0 + - @backstage/plugin-rollbar@0.4.16-next.0 + - @backstage/plugin-search@1.1.1-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + - @backstage/plugin-sentry@0.5.1-next.0 + - @backstage/plugin-shortcuts@0.3.8-next.0 + - @backstage/plugin-stack-overflow@0.1.12-next.0 + - @backstage/plugin-tech-insights@0.3.8-next.0 + - @backstage/plugin-tech-radar@0.6.2-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.11-next.0 + - @backstage/plugin-todo@0.2.18-next.0 + - @backstage/plugin-user-settings@0.7.1-next.0 + - @internal/plugin-catalog-customized@0.0.8-next.0 + +## 0.2.80 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0 + - @backstage/plugin-scaffolder@1.11.0 + - @backstage/plugin-azure-devops@0.2.6 + - @backstage/cli@0.22.2 + - @backstage/plugin-techdocs@1.5.0 + - @backstage/core-components@0.12.4 + - @backstage/plugin-microsoft-calendar@0.1.0 + - @backstage/plugin-catalog-graph@0.2.27 + - @backstage/plugin-code-coverage@0.2.8 + - @backstage/plugin-azure-sites@0.1.4 + - @backstage/plugin-lighthouse@0.4.0 + - @backstage/plugin-shortcuts@0.3.7 + - @backstage/plugin-home@0.4.31 + - @backstage/plugin-tech-insights@0.3.7 + - @backstage/plugin-search@1.1.0 + - @backstage/plugin-api-docs@0.9.0 + - @backstage/plugin-entity-feedback@0.1.0 + - @backstage/plugin-tech-radar@0.6.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10 + - @backstage/plugin-dynatrace@2.0.0 + - @backstage/plugin-playlist@0.1.6 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/plugin-kubernetes@0.7.8 + - @backstage/core-plugin-api@1.4.0 + - @backstage/app-defaults@1.2.0 + - @backstage/plugin-user-settings@0.7.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-pagerduty@0.5.8 + - @backstage/plugin-explore@0.4.0 + - @backstage/plugin-linguist@0.1.0 + - @backstage/plugin-linguist-common@0.1.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/plugin-apache-airflow@0.2.8 + - @backstage/plugin-circleci@0.3.15 + - @backstage/plugin-sentry@0.5.0 + - @backstage/plugin-org@0.6.5 + - @backstage/plugin-cost-insights@0.12.4 + - @backstage/plugin-airbrake@0.3.15 + - @internal/plugin-catalog-customized@0.0.7 + - @backstage/config@1.0.6 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-badges@0.2.39 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-catalog-import@0.9.5 + - @backstage/plugin-cloudbuild@0.3.15 + - @backstage/plugin-gcalendar@0.3.11 + - @backstage/plugin-gcp-projects@0.3.34 + - @backstage/plugin-github-actions@0.5.15 + - @backstage/plugin-gocd@0.1.21 + - @backstage/plugin-graphiql@0.2.47 + - @backstage/plugin-jenkins@0.7.14 + - @backstage/plugin-kafka@0.3.15 + - @backstage/plugin-newrelic@0.3.33 + - @backstage/plugin-newrelic-dashboard@0.2.8 + - @backstage/plugin-permission-react@0.4.10 + - @backstage/plugin-rollbar@0.4.15 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-stack-overflow@0.1.11 + - @backstage/plugin-techdocs-react@1.1.3 + - @backstage/plugin-todo@0.2.17 + +## 0.2.80-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.2 + - @backstage/plugin-scaffolder@1.11.0-next.2 + - @backstage/cli@0.22.2-next.1 + - @backstage/plugin-api-docs@0.9.0-next.2 + - @backstage/plugin-tech-radar@0.6.1-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-explore@0.4.0-next.2 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/plugin-sentry@0.5.0-next.2 + - @backstage/plugin-circleci@0.3.15-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/plugin-azure-sites@0.1.4-next.2 + - @backstage/plugin-search@1.1.0-next.2 + - @backstage/plugin-lighthouse@0.4.0-next.2 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-airbrake@0.3.15-next.2 + - @backstage/plugin-apache-airflow@0.2.8-next.1 + - @backstage/plugin-azure-devops@0.2.6-next.2 + - @backstage/plugin-badges@0.2.39-next.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-graph@0.2.27-next.2 + - @backstage/plugin-catalog-import@0.9.5-next.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-cloudbuild@0.3.15-next.2 + - @backstage/plugin-code-coverage@0.2.8-next.2 + - @backstage/plugin-cost-insights@0.12.4-next.2 + - @backstage/plugin-dynatrace@2.0.0-next.2 + - @backstage/plugin-gcalendar@0.3.11-next.1 + - @backstage/plugin-gcp-projects@0.3.34-next.1 + - @backstage/plugin-github-actions@0.5.15-next.2 + - @backstage/plugin-gocd@0.1.21-next.2 + - @backstage/plugin-graphiql@0.2.47-next.1 + - @backstage/plugin-home@0.4.31-next.2 + - @backstage/plugin-jenkins@0.7.14-next.2 + - @backstage/plugin-kafka@0.3.15-next.2 + - @backstage/plugin-kubernetes@0.7.8-next.2 + - @backstage/plugin-newrelic@0.3.33-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.8-next.2 + - @backstage/plugin-org@0.6.5-next.2 + - @backstage/plugin-pagerduty@0.5.8-next.2 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist@0.1.6-next.2 + - @backstage/plugin-rollbar@0.4.15-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-shortcuts@0.3.7-next.1 + - @backstage/plugin-stack-overflow@0.1.11-next.2 + - @backstage/plugin-tech-insights@0.3.7-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.2 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + - @backstage/plugin-todo@0.2.17-next.2 + - @backstage/plugin-user-settings@0.6.3-next.2 + - @internal/plugin-catalog-customized@0.0.7-next.2 + +## 0.2.80-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.1 + - @backstage/plugin-scaffolder@1.11.0-next.1 + - @backstage/cli@0.22.2-next.0 + - @backstage/plugin-techdocs@1.5.0-next.1 + - @backstage/plugin-tech-insights@0.3.7-next.1 + - @backstage/plugin-search@1.1.0-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.1 + - @backstage/plugin-dynatrace@2.0.0-next.1 + - @backstage/plugin-playlist@0.1.6-next.1 + - @backstage/plugin-home@0.4.31-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/plugin-kubernetes@0.7.8-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-tech-radar@0.6.1-next.0 + - @backstage/plugin-explore@0.4.0-next.1 + - @backstage/plugin-apache-airflow@0.2.8-next.0 + - @backstage/plugin-circleci@0.3.15-next.1 + - @backstage/plugin-sentry@0.4.8-next.1 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-airbrake@0.3.15-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/app-defaults@1.1.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-api-docs@0.8.15-next.1 + - @backstage/plugin-azure-devops@0.2.6-next.1 + - @backstage/plugin-azure-sites@0.1.4-next.1 + - @backstage/plugin-badges@0.2.39-next.1 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-graph@0.2.27-next.1 + - @backstage/plugin-catalog-import@0.9.5-next.1 + - @backstage/plugin-cloudbuild@0.3.15-next.1 + - @backstage/plugin-code-coverage@0.2.8-next.1 + - @backstage/plugin-cost-insights@0.12.4-next.1 + - @backstage/plugin-gcalendar@0.3.11-next.0 + - @backstage/plugin-gcp-projects@0.3.34-next.0 + - @backstage/plugin-github-actions@0.5.15-next.1 + - @backstage/plugin-gocd@0.1.21-next.1 + - @backstage/plugin-graphiql@0.2.47-next.0 + - @backstage/plugin-jenkins@0.7.14-next.1 + - @backstage/plugin-kafka@0.3.15-next.1 + - @backstage/plugin-lighthouse@0.3.15-next.1 + - @backstage/plugin-newrelic@0.3.33-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.8-next.1 + - @backstage/plugin-org@0.6.5-next.1 + - @backstage/plugin-pagerduty@0.5.8-next.1 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-rollbar@0.4.15-next.1 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-shortcuts@0.3.7-next.0 + - @backstage/plugin-stack-overflow@0.1.11-next.1 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + - @backstage/plugin-todo@0.2.17-next.1 + - @backstage/plugin-user-settings@0.6.3-next.1 + - @internal/plugin-catalog-customized@0.0.7-next.1 + +## 0.2.80-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-azure-devops@0.2.6-next.0 + - @backstage/plugin-scaffolder-react@1.1.0-next.0 + - @backstage/plugin-scaffolder@1.11.0-next.0 + - @backstage/plugin-api-docs@0.8.15-next.0 + - @backstage/plugin-techdocs@1.4.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/plugin-user-settings@0.6.3-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-kubernetes@0.7.8-next.0 + - @backstage/plugin-airbrake@0.3.15-next.0 + - @backstage/plugin-azure-sites@0.1.4-next.0 + - @backstage/plugin-badges@0.2.39-next.0 + - @internal/plugin-catalog-customized@0.0.7-next.0 + - @backstage/plugin-catalog-graph@0.2.27-next.0 + - @backstage/plugin-catalog-import@0.9.5-next.0 + - @backstage/plugin-circleci@0.3.15-next.0 + - @backstage/plugin-cloudbuild@0.3.15-next.0 + - @backstage/plugin-code-coverage@0.2.8-next.0 + - @backstage/plugin-cost-insights@0.12.4-next.0 + - @backstage/plugin-dynatrace@2.0.0-next.0 + - @backstage/plugin-explore@0.3.46-next.0 + - @backstage/plugin-github-actions@0.5.15-next.0 + - @backstage/plugin-gocd@0.1.21-next.0 + - @backstage/plugin-home@0.4.31-next.0 + - @backstage/plugin-jenkins@0.7.14-next.0 + - @backstage/plugin-kafka@0.3.15-next.0 + - @backstage/plugin-lighthouse@0.3.15-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.8-next.0 + - @backstage/plugin-org@0.6.5-next.0 + - @backstage/plugin-pagerduty@0.5.8-next.0 + - @backstage/plugin-playlist@0.1.6-next.0 + - @backstage/plugin-rollbar@0.4.15-next.0 + - @backstage/plugin-search@1.0.8-next.0 + - @backstage/plugin-sentry@0.4.8-next.0 + - @backstage/plugin-tech-insights@0.3.7-next.0 + - @backstage/plugin-todo@0.2.17-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/cli@0.22.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.0 + - @backstage/integration-react@1.1.9 + - @backstage/plugin-apache-airflow@0.2.7 + - @backstage/plugin-gcalendar@0.3.10 + - @backstage/plugin-gcp-projects@0.3.33 + - @backstage/plugin-graphiql@0.2.46 + - @backstage/plugin-newrelic@0.3.32 + - @backstage/plugin-shortcuts@0.3.6 + - @backstage/plugin-stack-overflow@0.1.11-next.0 + - @backstage/plugin-tech-radar@0.6.0 + ## 0.2.79 ### Patch Changes diff --git a/packages/app/cypress/.eslintrc.json b/packages/app/cypress/.eslintrc.json index 2b3a458b95..a467608916 100644 --- a/packages/app/cypress/.eslintrc.json +++ b/packages/app/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/packages/app/package.json b/packages/app/package.json index 2684d5ccb6..7aba203895 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.79", + "version": "0.2.81-next.0", "private": true, "backstage": { "role": "frontend" @@ -30,6 +30,7 @@ "@backstage/plugin-code-coverage": "workspace:^", "@backstage/plugin-cost-insights": "workspace:^", "@backstage/plugin-dynatrace": "workspace:^", + "@backstage/plugin-entity-feedback": "workspace:^", "@backstage/plugin-explore": "workspace:^", "@backstage/plugin-gcalendar": "workspace:^", "@backstage/plugin-gcp-projects": "workspace:^", @@ -41,6 +42,9 @@ "@backstage/plugin-kafka": "workspace:^", "@backstage/plugin-kubernetes": "workspace:^", "@backstage/plugin-lighthouse": "workspace:^", + "@backstage/plugin-linguist": "workspace:^", + "@backstage/plugin-linguist-common": "workspace:^", + "@backstage/plugin-microsoft-calendar": "workspace:^", "@backstage/plugin-newrelic": "workspace:^", "@backstage/plugin-newrelic-dashboard": "workspace:^", "@backstage/plugin-org": "workspace:^", @@ -56,6 +60,7 @@ "@backstage/plugin-sentry": "workspace:^", "@backstage/plugin-shortcuts": "workspace:^", "@backstage/plugin-stack-overflow": "workspace:^", + "@backstage/plugin-stackstorm": "workspace:^", "@backstage/plugin-tech-insights": "workspace:^", "@backstage/plugin-tech-radar": "workspace:^", "@backstage/plugin-techdocs": "workspace:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c5410c4f80..4dacbbc007 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -59,9 +59,9 @@ import { GraphiQLPage } from '@backstage/plugin-graphiql'; import { HomepageCompositionRoot } from '@backstage/plugin-home'; import { LighthousePage } from '@backstage/plugin-lighthouse'; import { NewRelicPage } from '@backstage/plugin-newrelic'; +import { NextScaffolderPage } from '@backstage/plugin-scaffolder/alpha'; import { ScaffolderPage, - NextScaffolderPage, scaffolderPlugin, ScaffolderLayouts, } from '@backstage/plugin-scaffolder'; @@ -78,6 +78,7 @@ import { ExpandableNavigation, ReportIssue, TextSize, + LightBox, } from '@backstage/plugin-techdocs-module-addons-contrib'; import { SettingsLayout, @@ -103,10 +104,11 @@ import * as plugins from './plugins'; import { techDocsPage } from './components/techdocs/TechDocsPage'; import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; import { RequirePermission } from '@backstage/plugin-permission-react'; -import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; import { PlaylistIndexPage } from '@backstage/plugin-playlist'; import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts'; import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card'; +import { StackstormPage } from '@backstage/plugin-stackstorm'; const app = createApp({ apis, @@ -203,6 +205,7 @@ const routes = ( <ExpandableNavigation /> <ReportIssue /> <TextSize /> + <LightBox /> </TechDocsAddons> </Route> <Route @@ -244,6 +247,9 @@ const routes = ( <ScaffolderFieldExtensions> <DelayingComponentFieldExtension /> </ScaffolderFieldExtensions> + <ScaffolderLayouts> + <TwoColumnLayout /> + </ScaffolderLayouts> </Route> <Route path="/explore" element={<ExplorePage />} /> <Route @@ -276,6 +282,7 @@ const routes = ( <Route path="/apache-airflow" element={<ApacheAirflowPage />} /> <Route path="/playlist" element={<PlaylistIndexPage />} /> <Route path="/score-board" element={<ScoreBoardPage />} /> + <Route path="/stackstorm" element={<StackstormPage />} /> </FlatRoutes> ); diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 3566800f0b..d07870194b 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -25,7 +25,7 @@ import { RELATION_PART_OF, RELATION_PROVIDES_API, } from '@backstage/catalog-model'; -import { EmptyState } from '@backstage/core-components'; +import { EmptyState, InfoCard } from '@backstage/core-components'; import { EntityApiDefinitionCard, EntityConsumedApisCard, @@ -79,6 +79,11 @@ import { DynatraceTab, isDynatraceAvailable, } from '@backstage/plugin-dynatrace'; +import { + EntityFeedbackResponseContent, + EntityLikeDislikeRatingsCard, + LikeDislikeButtons, +} from '@backstage/plugin-entity-feedback'; import { EntityGithubActionsContent, EntityRecentGithubActionsRunsCard, @@ -154,8 +159,13 @@ import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { TextSize, ReportIssue, + LightBox, } from '@backstage/plugin-techdocs-module-addons-contrib'; import { EntityCostInsightsContent } from '@backstage/plugin-cost-insights'; +import { + isLinguistAvailable, + EntityLinguistCard, +} from '@backstage/plugin-linguist'; const customEntityFilterKind = ['Component', 'API', 'System']; @@ -205,6 +215,7 @@ const techdocsContent = ( <TechDocsAddons> <TextSize /> <ReportIssue /> + <LightBox /> </TechDocsAddons> </EntityTechdocsContent> ); @@ -374,6 +385,12 @@ const overviewContent = ( </EntitySwitch.Case> </EntitySwitch> + <Grid item md={2}> + <InfoCard title="Rate this entity"> + <LikeDislikeButtons /> + </InfoCard> + </Grid> + {cicdCard} <EntitySwitch> @@ -404,6 +421,14 @@ const overviewContent = ( </EntitySwitch.Case> </EntitySwitch> + <EntitySwitch> + <EntitySwitch.Case if={isLinguistAvailable}> + <Grid item md={6}> + <EntityLinguistCard /> + </Grid> + </EntitySwitch.Case> + </EntitySwitch> + <Grid item md={8} xs={12}> <EntityHasSubcomponentsCard variant="gridItem" /> </Grid> @@ -511,6 +536,10 @@ const serviceEntityPage = ( > <DynatraceTab /> </EntityLayout.Route> + + <EntityLayout.Route path="/feedback" title="Feedback"> + <EntityFeedbackResponseContent /> + </EntityLayout.Route> </EntityLayoutWrapper> ); @@ -590,6 +619,10 @@ const websiteEntityPage = ( <EntityLayout.Route path="/todos" title="TODOs"> <EntityTodoContent /> </EntityLayout.Route> + + <EntityLayout.Route path="/feedback" title="Feedback"> + <EntityFeedbackResponseContent /> + </EntityLayout.Route> </EntityLayoutWrapper> ); @@ -606,6 +639,10 @@ const defaultEntityPage = ( <EntityLayout.Route path="/todos" title="TODOs"> <EntityTodoContent /> </EntityLayout.Route> + + <EntityLayout.Route path="/feedback" title="Feedback"> + <EntityFeedbackResponseContent /> + </EntityLayout.Route> </EntityLayoutWrapper> ); @@ -642,6 +679,11 @@ const apiPage = ( <Grid item xs={12} md={6}> <EntityConsumingComponentsCard /> </Grid> + <Grid item md={2}> + <InfoCard title="Rate this entity"> + <LikeDislikeButtons /> + </InfoCard> + </Grid> </Grid> </Grid> </Grid> @@ -654,6 +696,10 @@ const apiPage = ( </Grid> </Grid> </EntityLayout.Route> + + <EntityLayout.Route path="/feedback" title="Feedback"> + <EntityFeedbackResponseContent /> + </EntityLayout.Route> </EntityLayoutWrapper> ); @@ -671,6 +717,9 @@ const userPage = ( entityFilterKind={customEntityFilterKind} /> </Grid> + <Grid item xs={12}> + <EntityLikeDislikeRatingsCard /> + </Grid> </Grid> </EntityLayout.Route> </EntityLayoutWrapper> @@ -693,6 +742,9 @@ const groupPage = ( <Grid item xs={12}> <EntityMembersListCard /> </Grid> + <Grid item xs={12}> + <EntityLikeDislikeRatingsCard /> + </Grid> </Grid> </EntityLayout.Route> </EntityLayoutWrapper> @@ -718,6 +770,11 @@ const systemPage = ( <Grid item md={6}> <EntityHasResourcesCard variant="gridItem" /> </Grid> + <Grid item md={2}> + <InfoCard title="Rate this entity"> + <LikeDislikeButtons /> + </InfoCard> + </Grid> </Grid> </EntityLayout.Route> <EntityLayout.Route path="/score" title="Score"> @@ -746,6 +803,9 @@ const systemPage = ( unidirectional={false} /> </EntityLayout.Route> + <EntityLayout.Route path="/feedback" title="Feedback"> + <EntityFeedbackResponseContent /> + </EntityLayout.Route> </EntityLayoutWrapper> ); @@ -763,8 +823,16 @@ const domainPage = ( <Grid item md={6}> <EntityHasSystemsCard variant="gridItem" /> </Grid> + <Grid item md={2}> + <InfoCard title="Rate this entity"> + <LikeDislikeButtons /> + </InfoCard> + </Grid> </Grid> </EntityLayout.Route> + <EntityLayout.Route path="/feedback" title="Feedback"> + <EntityFeedbackResponseContent /> + </EntityLayout.Route> </EntityLayoutWrapper> ); diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index e432e1a00e..29023738af 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -27,6 +27,7 @@ import { import { Content, Header, Page } from '@backstage/core-components'; import { HomePageSearchBar } from '@backstage/plugin-search'; import { HomePageCalendar } from '@backstage/plugin-gcalendar'; +import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar'; import Grid from '@material-ui/core/Grid'; import React from 'react'; @@ -114,6 +115,9 @@ export const homePage = ( <Grid item xs={12} md={4}> <HomePageCalendar /> </Grid> + <Grid item xs={12} md={4}> + <MicrosoftCalendarCard /> + </Grid> <Grid item xs={12} md={4}> <HomePageStarredEntities /> </Grid> diff --git a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx index c521bcf86c..b15d12979a 100644 --- a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx +++ b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx @@ -17,10 +17,11 @@ import React from 'react'; import type { FieldValidation } from '@rjsf/utils'; import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; import { TextField } from '@material-ui/core'; - import { NextFieldExtensionComponentProps, createNextScaffolderFieldExtension, +} from '@backstage/plugin-scaffolder-react/alpha'; +import { createScaffolderFieldExtension, FieldExtensionComponentProps, } from '@backstage/plugin-scaffolder-react'; @@ -68,7 +69,7 @@ export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide( const MockDelayComponent = ( props: NextFieldExtensionComponentProps<{ test?: string }>, ) => { - const { onChange, formData, rawErrors } = props; + const { onChange, formData, rawErrors = [] } = props; return ( <TextField label="test" diff --git a/packages/app/src/components/search/SearchModal.tsx b/packages/app/src/components/search/SearchModal.tsx index 527351a857..ea5a156c55 100644 --- a/packages/app/src/components/search/SearchModal.tsx +++ b/packages/app/src/components/search/SearchModal.tsx @@ -21,20 +21,13 @@ import { DialogContent, DialogTitle, Grid, - List, makeStyles, Paper, - useTheme, } from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; import BuildIcon from '@material-ui/icons/Build'; import LaunchIcon from '@material-ui/icons/Launch'; -import { - CatalogIcon, - DocsIcon, - Link, - useContent, -} from '@backstage/core-components'; +import { CatalogIcon, DocsIcon, Link } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized'; import { @@ -44,9 +37,8 @@ import { import { ToolSearchResultListItem } from '@backstage/plugin-explore'; import { searchPlugin, SearchType } from '@backstage/plugin-search'; import { - DefaultResultListItem, - SearchFilter, SearchBar, + SearchFilter, SearchResult, SearchResultPager, useSearch, @@ -80,9 +72,6 @@ const rootRouteRef = searchPlugin.routes.root; export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { const classes = useStyles(); const navigate = useNavigate(); - const { transitions } = useTheme(); - const { focusContent } = useContent(); - const catalogApi = useApi(catalogApiRef); const { term, types } = useSearch(); @@ -93,19 +82,14 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { searchBarRef?.current?.focus(); }); - const handleSearchResulClick = useCallback(() => { - toggleModal(); - setTimeout(focusContent, transitions.duration.leavingScreen); - }, [toggleModal, focusContent, transitions]); - const handleSearchBarKeyDown = useCallback( (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => { if (e.key === 'Enter') { - navigate(searchPagePath); toggleModal(); + navigate(searchPagePath); } }, - [navigate, toggleModal, searchPagePath], + [navigate, searchPagePath, toggleModal], ); return ( @@ -189,7 +173,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { alignItems="center" > <Grid item> - <Link to={searchPagePath} onClick={handleSearchResulClick}> + <Link to={searchPagePath} onClick={toggleModal}> <Typography component="span" className={classes.viewResultsLink} @@ -202,69 +186,10 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { </Grid> </Grid> <Grid item xs> - <SearchResult> - {({ results }) => ( - <List> - {results.map(({ type, document, highlight, rank }) => { - let resultItem; - switch (type) { - case 'software-catalog': - resultItem = ( - <CatalogSearchResultListItem - icon={<CatalogIcon />} - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - break; - case 'techdocs': - resultItem = ( - <TechDocsSearchResultListItem - icon={<DocsIcon />} - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - break; - case 'tools': - resultItem = ( - <ToolSearchResultListItem - icon={<BuildIcon />} - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - break; - default: - resultItem = ( - <DefaultResultListItem - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - } - return ( - <div - role="button" - tabIndex={0} - key={`${document.location}-btn`} - onClick={handleSearchResulClick} - onKeyDown={handleSearchResulClick} - > - {resultItem} - </div> - ); - })} - </List> - )} + <SearchResult onClick={toggleModal}> + <CatalogSearchResultListItem icon={<CatalogIcon />} /> + <TechDocsSearchResultListItem icon={<DocsIcon />} /> + <ToolSearchResultListItem icon={<BuildIcon />} /> </SearchResult> </Grid> </Grid> diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index b79c6a7248..6c4f06aed9 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -30,16 +30,15 @@ import { } from '@backstage/plugin-catalog-react'; import { SearchType } from '@backstage/plugin-search'; import { - DefaultResultListItem, SearchBar, SearchFilter, - SearchResult, SearchPagination, + SearchResult, SearchResultPager, useSearch, } from '@backstage/plugin-search-react'; import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; -import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; +import { Grid, makeStyles, Paper, Theme } from '@material-ui/core'; import React from 'react'; import { ToolSearchResultListItem } from '@backstage/plugin-explore'; import BuildIcon from '@material-ui/icons/Build'; @@ -80,6 +79,7 @@ const SearchPage = () => { <SearchType.Accordion name="Result Type" defaultValue="software-catalog" + showCounts types={[ { value: 'software-catalog', @@ -133,53 +133,9 @@ const SearchPage = () => { <Grid item xs> <SearchPagination /> <SearchResult> - {({ results }) => ( - <List> - {results.map(({ type, document, highlight, rank }) => { - switch (type) { - case 'software-catalog': - return ( - <CatalogSearchResultListItem - icon={<CatalogIcon />} - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - case 'techdocs': - return ( - <TechDocsSearchResultListItem - icon={<DocsIcon />} - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - case 'tools': - return ( - <ToolSearchResultListItem - icon={<BuildIcon />} - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - default: - return ( - <DefaultResultListItem - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - } - })} - </List> - )} + <CatalogSearchResultListItem icon={<CatalogIcon />} /> + <TechDocsSearchResultListItem icon={<DocsIcon />} /> + <ToolSearchResultListItem icon={<BuildIcon />} /> </SearchResult> <SearchResultPager /> </Grid> diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 5f8c0faf47..66f1460210 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -22,6 +22,7 @@ import { microsoftAuthApiRef, oneloginAuthApiRef, bitbucketAuthApiRef, + bitbucketServerAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ @@ -67,4 +68,10 @@ export const providers = [ message: 'Sign In using Bitbucket', apiRef: bitbucketAuthApiRef, }, + { + id: 'bitbucket-server-auth-provider', + title: 'Bitbucket Server', + message: 'Sign In using Bitbucket Server', + apiRef: bitbucketServerAuthApiRef, + }, ]; diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index 5195160f9f..af7828daac 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,112 @@ # @backstage/backend-app-api +## 0.4.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-node@0.7.6-next.0 + +## 0.4.0 + +### Minor Changes + +- 01a075ec1d: **BREAKING**: Renamed `RootHttpRouterConfigureOptions` to `RootHttpRouterConfigureContext`, and removed the unused type `ServiceOrExtensionPoint`. +- 4ae71b7f2e: **BREAKING** Renaming `*Factory` exports to `*ServiceFactory` instead. For example `configFactory` now is exported as `configServiceFactory`. +- d31d8e00b3: **BREAKING** `HttpServerCertificateOptions` when specified with a `key` and `cert` should also have the `type: 'pem'` instead of `type: 'plain'` + +### Patch Changes + +- a18da2f8b5: Fixed an issue were the log redaction didn't properly escape RegExp characters. +- 5febb216fe: Updated to match the new `CacheService` interface. +- e716946103: Updated usage of the lifecycle service. +- f60cca9da1: Updated database factory to pass service deps required for restoring database state during development. +- 610d65e143: Updates to match new `BackendFeature` type. +- 725383f69d: Tweaked messaging in the README. +- b86efa2d04: Updated usage of `ServiceFactory`. +- ab22515647: The shutdown signal handlers are now installed as part of the backend instance rather than the lifecycle service, and explicitly cause the process to exit. +- b729f9f31f: Moved the options of the `config` and `rootHttpRouter` services out to the factories themselves, where they belong +- ed8b5967d7: `HttpRouterFactoryOptions.getPath` is now optional as a default value is always provided in the factory. +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/backend-tasks@0.4.3 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-node@0.7.5 + +## 0.4.0-next.2 + +### Minor Changes + +- 01a075ec1d: **BREAKING**: Renamed `RootHttpRouterConfigureOptions` to `RootHttpRouterConfigureContext`, and removed the unused type `ServiceOrExtensionPoint`. +- 4ae71b7f2e: **BREAKING** Renaming `*Factory` exports to `*ServiceFactory` instead. For example `configFactory` now is exported as `configServiceFactory`. +- d31d8e00b3: **BREAKING** `HttpServerCertificateOptions` when specified with a `key` and `cert` should also have the `type: 'pem'` instead of `type: 'plain'` + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- f60cca9da1: Updated database factory to pass service deps required for restoring database state during development. +- 610d65e143: Updates to match new `BackendFeature` type. +- ab22515647: The shutdown signal handlers are now installed as part of the backend instance rather than the lifecycle service, and explicitly cause the process to exit. +- b729f9f31f: Moved the options of the `config` and `rootHttpRouter` services out to the factories themselves, where they belong +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-node@0.7.5-next.1 + +## 0.3.2-next.0 + +### Patch Changes + +- a18da2f8b5: Fixed an issue were the log redaction didn't properly escape RegExp characters. +- ed8b5967d7: `HttpRouterFactoryOptions.getPath` is now optional as a default value is always provided in the factory. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.3.0 ### Minor Changes diff --git a/packages/backend-app-api/README.md b/packages/backend-app-api/README.md index ee608ceb43..e5b80abeaf 100644 --- a/packages/backend-app-api/README.md +++ b/packages/backend-app-api/README.md @@ -1,6 +1,6 @@ # @backstage/backend-app-api -**This package is HIGHLY EXPERIMENTAL, do not use this for production** +**This package is EXPERIMENTAL, we recommend against using it for production deployments** This package provides the core API used by Backstage backend apps. @@ -16,4 +16,5 @@ yarn add --cwd packages/backend @backstage/backend-app-api ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) +- [Backstage Backend System](https://backstage.io/docs/backend-system/) diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index dcdc105b17..e99135b3ec 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -6,24 +6,23 @@ /// <reference types="node" /> import { BackendFeature } from '@backstage/backend-plugin-api'; +import { CacheClient } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import { ConfigService } from '@backstage/backend-plugin-api'; import { CorsOptions } from 'cors'; import { ErrorRequestHandler } from 'express'; import { Express as Express_2 } from 'express'; -import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { Format } from 'logform'; import { Handler } from 'express'; import { HelmetOptions } from 'helmet'; import * as http from 'http'; import { HttpRouterService } from '@backstage/backend-plugin-api'; import { IdentityService } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { LogMeta } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; -import { PluginCacheManager } from '@backstage/backend-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { RequestHandler } from 'express'; @@ -34,7 +33,6 @@ import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; -import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { transport } from 'winston'; import { UrlReader } from '@backstage/backend-common'; @@ -50,10 +48,7 @@ export interface Backend { } // @public (undocumented) -export const cacheFactory: () => ServiceFactory<PluginCacheManager>; - -// @public (undocumented) -export const configFactory: () => ServiceFactory<ConfigService>; +export const cacheServiceFactory: () => ServiceFactory<CacheClient, 'plugin'>; // @public (undocumented) export interface ConfigFactoryOptions { @@ -61,6 +56,11 @@ export interface ConfigFactoryOptions { remote?: LoadConfigOptionsRemote; } +// @public (undocumented) +export const configServiceFactory: ( + options?: ConfigFactoryOptions | undefined, +) => ServiceFactory<ConfigService, 'root'>; + // @public (undocumented) export function createConfigSecretEnumerator(options: { logger: LoggerService; @@ -88,7 +88,10 @@ export interface CreateSpecializedBackendOptions { } // @public (undocumented) -export const databaseFactory: () => ServiceFactory<PluginDatabaseManager>; +export const databaseServiceFactory: () => ServiceFactory< + PluginDatabaseManager, + 'plugin' +>; // @public export class DefaultRootHttpRouter implements RootHttpRouterService { @@ -106,7 +109,10 @@ export interface DefaultRootHttpRouterOptions { } // @public (undocumented) -export const discoveryFactory: () => ServiceFactory<PluginEndpointDiscovery>; +export const discoveryServiceFactory: () => ServiceFactory< + PluginEndpointDiscovery, + 'plugin' +>; // @public export interface ExtendedHttpServer extends http.Server { @@ -119,19 +125,19 @@ export interface ExtendedHttpServer extends http.Server { } // @public (undocumented) -export const httpRouterFactory: ( - options?: HttpRouterFactoryOptions | undefined, -) => ServiceFactory<HttpRouterService>; +export interface HttpRouterFactoryOptions { + getPath?(pluginId: string): string; +} // @public (undocumented) -export interface HttpRouterFactoryOptions { - getPath(pluginId: string): string; -} +export const httpRouterServiceFactory: ( + options?: HttpRouterFactoryOptions | undefined, +) => ServiceFactory<HttpRouterService, 'plugin'>; // @public export type HttpServerCertificateOptions = | { - type: 'plain'; + type: 'pem'; key: string; cert: string; } @@ -151,19 +157,22 @@ export type HttpServerOptions = { }; }; -// @public (undocumented) -export const identityFactory: ( - options?: IdentityFactoryOptions | undefined, -) => ServiceFactory<IdentityService>; - // @public export type IdentityFactoryOptions = { issuer?: string; algorithms?: string[]; }; +// @public (undocumented) +export const identityServiceFactory: ( + options?: IdentityFactoryOptions | undefined, +) => ServiceFactory<IdentityService, 'plugin'>; + // @public -export const lifecycleFactory: () => ServiceFactory<LifecycleService>; +export const lifecycleServiceFactory: () => ServiceFactory< + LifecycleService, + 'plugin' +>; // @public export function loadBackendConfig(options: { @@ -174,7 +183,10 @@ export function loadBackendConfig(options: { }>; // @public (undocumented) -export const loggerFactory: () => ServiceFactory<LoggerService>; +export const loggerServiceFactory: () => ServiceFactory< + LoggerService, + 'plugin' +>; // @public export class MiddlewareFactory { @@ -202,7 +214,10 @@ export interface MiddlewareFactoryOptions { } // @public (undocumented) -export const permissionsFactory: () => ServiceFactory<PermissionsService>; +export const permissionsServiceFactory: () => ServiceFactory< + PermissionsService, + 'plugin' +>; // @public export function readCorsOptions(config?: Config): CorsOptions; @@ -214,7 +229,7 @@ export function readHelmetOptions(config?: Config): HelmetOptions; export function readHttpServerOptions(config?: Config): HttpServerOptions; // @public (undocumented) -export interface RootHttpRouterConfigureOptions { +export interface RootHttpRouterConfigureContext { // (undocumented) app: Express_2; // (undocumented) @@ -229,55 +244,64 @@ export interface RootHttpRouterConfigureOptions { routes: RequestHandler; } -// @public (undocumented) -export const rootHttpRouterFactory: () => ServiceFactory<RootHttpRouterService>; - // @public (undocumented) export type RootHttpRouterFactoryOptions = { indexPath?: string | false; - configure?(options: RootHttpRouterConfigureOptions): void; + configure?(context: RootHttpRouterConfigureContext): void; }; +// @public (undocumented) +export const rootHttpRouterServiceFactory: ( + options?: RootHttpRouterFactoryOptions | undefined, +) => ServiceFactory<RootHttpRouterService, 'root'>; + // @public -export const rootLifecycleFactory: () => ServiceFactory<RootLifecycleService>; +export const rootLifecycleServiceFactory: () => ServiceFactory< + RootLifecycleService, + 'root' +>; // @public (undocumented) -export const rootLoggerFactory: () => ServiceFactory<RootLoggerService>; +export const rootLoggerServiceFactory: () => ServiceFactory< + RootLoggerService, + 'root' +>; // @public (undocumented) -export const schedulerFactory: () => ServiceFactory<SchedulerService>; +export const schedulerServiceFactory: () => ServiceFactory< + SchedulerService, + 'plugin' +>; // @public (undocumented) -export type ServiceOrExtensionPoint<T = unknown> = - | ExtensionPoint<T> - | ServiceRef<T>; +export const tokenManagerServiceFactory: () => ServiceFactory< + TokenManagerService, + 'plugin' +>; // @public (undocumented) -export const tokenManagerFactory: () => ServiceFactory<TokenManagerService>; - -// @public (undocumented) -export const urlReaderFactory: () => ServiceFactory<UrlReader>; +export const urlReaderServiceFactory: () => ServiceFactory<UrlReader, 'plugin'>; // @public export class WinstonLogger implements RootLoggerService { // (undocumented) addRedactions(redactions: Iterable<string>): void; // (undocumented) - child(meta: LogMeta): LoggerService; + child(meta: JsonObject): LoggerService; static colorFormat(): Format; static create(options: WinstonLoggerOptions): WinstonLogger; // (undocumented) - debug(message: string, meta?: LogMeta): void; + debug(message: string, meta?: JsonObject): void; // (undocumented) - error(message: string, meta?: LogMeta): void; + error(message: string, meta?: JsonObject): void; // (undocumented) - info(message: string, meta?: LogMeta): void; + info(message: string, meta?: JsonObject): void; static redacter(): { format: Format; add: (redactions: Iterable<string>) => void; }; // (undocumented) - warn(message: string, meta?: LogMeta): void; + warn(message: string, meta?: JsonObject): void; } // @public (undocumented) @@ -287,7 +311,7 @@ export interface WinstonLoggerOptions { // (undocumented) level: string; // (undocumented) - meta?: LogMeta; + meta?: JsonObject; // (undocumented) transports: transport[]; } diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 93aa767ddb..099466aef7 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,14 +1,13 @@ { "name": "@backstage/backend-app-api", "description": "Core API used by Backstage backend apps", - "version": "0.3.0", + "version": "0.4.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "types": "dist/index.d.ts" }, "backstage": { "role": "node-library" @@ -24,7 +23,7 @@ ], "license": "Apache-2.0", "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -64,6 +63,7 @@ "winston-transport": "^4.5.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/compression": "^1.7.0", "@types/fs-extra": "^9.0.3", diff --git a/packages/backend-app-api/src/http/MiddlewareFactory.ts b/packages/backend-app-api/src/http/MiddlewareFactory.ts index dc9fe9aa5f..833bdba540 100644 --- a/packages/backend-app-api/src/http/MiddlewareFactory.ts +++ b/packages/backend-app-api/src/http/MiddlewareFactory.ts @@ -38,6 +38,7 @@ import { NotModifiedError, serializeError, } from '@backstage/errors'; +import { NotImplementedError } from '@backstage/errors'; /** * Options used to create a {@link MiddlewareFactory}. @@ -257,6 +258,8 @@ function getStatusCode(error: Error): number { return 404; case ConflictError.name: return 409; + case NotImplementedError.name: + return 501; default: break; } diff --git a/packages/backend-app-api/src/http/config.test.ts b/packages/backend-app-api/src/http/config.test.ts index f54ba927ca..577bae08f3 100644 --- a/packages/backend-app-api/src/http/config.test.ts +++ b/packages/backend-app-api/src/http/config.test.ts @@ -53,7 +53,7 @@ describe('readHttpServerOptions', () => { { listen: { host: '', port: 7007 }, https: { - certificate: { type: 'plain', cert: 'my-cert', key: 'my-key' }, + certificate: { type: 'pem', cert: 'my-cert', key: 'my-key' }, }, }, ], diff --git a/packages/backend-app-api/src/http/config.ts b/packages/backend-app-api/src/http/config.ts index e3b41c63de..ec29a8a044 100644 --- a/packages/backend-app-api/src/http/config.ts +++ b/packages/backend-app-api/src/http/config.ts @@ -93,7 +93,7 @@ function readHttpsOptions(config?: Config): HttpServerOptions['https'] { return { certificate: { - type: 'plain', + type: 'pem', cert: cc.getString('certificate.cert'), key: cc.getString('certificate.key'), }, diff --git a/packages/backend-app-api/src/http/types.ts b/packages/backend-app-api/src/http/types.ts index ff96f69bff..2298ef11ff 100644 --- a/packages/backend-app-api/src/http/types.ts +++ b/packages/backend-app-api/src/http/types.ts @@ -51,7 +51,7 @@ export type HttpServerOptions = { */ export type HttpServerCertificateOptions = | { - type: 'plain'; + type: 'pem'; key: string; cert: string; } diff --git a/packages/backend-app-api/src/logging/WinstonLogger.ts b/packages/backend-app-api/src/logging/WinstonLogger.ts index d32b816523..06a189508b 100644 --- a/packages/backend-app-api/src/logging/WinstonLogger.ts +++ b/packages/backend-app-api/src/logging/WinstonLogger.ts @@ -16,9 +16,9 @@ import { LoggerService, - LogMeta, RootLoggerService, } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; import { Format, TransformableInfo } from 'logform'; import { Logger, @@ -33,7 +33,7 @@ import { escapeRegExp } from '../lib/escapeRegExp'; * @public */ export interface WinstonLoggerOptions { - meta?: LogMeta; + meta?: JsonObject; level: string; format: Format; transports: Transport[]; @@ -150,23 +150,23 @@ export class WinstonLogger implements RootLoggerService { this.#addRedactions = addRedactions; } - error(message: string, meta?: LogMeta): void { + error(message: string, meta?: JsonObject): void { this.#winston.error(message, meta); } - warn(message: string, meta?: LogMeta): void { + warn(message: string, meta?: JsonObject): void { this.#winston.warn(message, meta); } - info(message: string, meta?: LogMeta): void { + info(message: string, meta?: JsonObject): void { this.#winston.info(message, meta); } - debug(message: string, meta?: LogMeta): void { + debug(message: string, meta?: JsonObject): void { this.#winston.debug(message, meta); } - child(meta: LogMeta): LoggerService { + child(meta: JsonObject): LoggerService { return new WinstonLogger(this.#winston.child(meta)); } diff --git a/packages/backend-app-api/src/services/implementations/cache/cacheFactory.ts b/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts similarity index 89% rename from packages/backend-app-api/src/services/implementations/cache/cacheFactory.ts rename to packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts index 426a890c00..2208249280 100644 --- a/packages/backend-app-api/src/services/implementations/cache/cacheFactory.ts +++ b/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-plugin-api'; /** @public */ -export const cacheFactory = createServiceFactory({ +export const cacheServiceFactory = createServiceFactory({ service: coreServices.cache, deps: { config: coreServices.config, @@ -31,6 +31,6 @@ export const cacheFactory = createServiceFactory({ return CacheManager.fromConfig(config); }, async factory({ plugin }, manager) { - return manager.forPlugin(plugin.getId()); + return manager.forPlugin(plugin.getId()).getClient(); }, }); diff --git a/packages/backend-app-api/src/services/implementations/cache/index.ts b/packages/backend-app-api/src/services/implementations/cache/index.ts index 3d1bcd75a7..f96ee77182 100644 --- a/packages/backend-app-api/src/services/implementations/cache/index.ts +++ b/packages/backend-app-api/src/services/implementations/cache/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { cacheFactory } from './cacheFactory'; +export { cacheServiceFactory } from './cacheServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/config/configFactory.ts b/packages/backend-app-api/src/services/implementations/config/configServiceFactory.ts similarity index 76% rename from packages/backend-app-api/src/services/implementations/config/configFactory.ts rename to packages/backend-app-api/src/services/implementations/config/configServiceFactory.ts index 0ec15c29fe..459c0d9f0c 100644 --- a/packages/backend-app-api/src/services/implementations/config/configFactory.ts +++ b/packages/backend-app-api/src/services/implementations/config/configServiceFactory.ts @@ -35,13 +35,14 @@ export interface ConfigFactoryOptions { } /** @public */ -export const configFactory = createServiceFactory({ - service: coreServices.config, - deps: {}, - async factory({}, options?: ConfigFactoryOptions) { - const { argv = process.argv, remote } = options ?? {}; - - const { config } = await loadBackendConfig({ argv, remote }); - return config; - }, -}); +export const configServiceFactory = createServiceFactory( + (options?: ConfigFactoryOptions) => ({ + service: coreServices.config, + deps: {}, + async factory({}) { + const { argv = process.argv, remote } = options ?? {}; + const { config } = await loadBackendConfig({ argv, remote }); + return config; + }, + }), +); diff --git a/packages/backend-app-api/src/services/implementations/config/index.ts b/packages/backend-app-api/src/services/implementations/config/index.ts index b23c47ce0a..690f1e2d36 100644 --- a/packages/backend-app-api/src/services/implementations/config/index.ts +++ b/packages/backend-app-api/src/services/implementations/config/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { configFactory } from './configFactory'; -export type { ConfigFactoryOptions } from './configFactory'; +export { configServiceFactory } from './configServiceFactory'; +export type { ConfigFactoryOptions } from './configServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/database/databaseFactory.ts b/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts similarity index 79% rename from packages/backend-app-api/src/services/implementations/database/databaseFactory.ts rename to packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts index 8baf84cb69..05e2f1deb9 100644 --- a/packages/backend-app-api/src/services/implementations/database/databaseFactory.ts +++ b/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts @@ -22,11 +22,12 @@ import { import { ConfigReader } from '@backstage/config'; /** @public */ -export const databaseFactory = createServiceFactory({ +export const databaseServiceFactory = createServiceFactory({ service: coreServices.database, deps: { config: coreServices.config, - plugin: coreServices.pluginMetadata, + lifecycle: coreServices.lifecycle, + pluginMetadata: coreServices.pluginMetadata, }, async createRootContext({ config }) { return config.getOptional('backend.database') @@ -39,7 +40,10 @@ export const databaseFactory = createServiceFactory({ }), ); }, - async factory({ plugin }, databaseManager) { - return databaseManager.forPlugin(plugin.getId()); + async factory({ pluginMetadata, lifecycle }, databaseManager) { + return databaseManager.forPlugin(pluginMetadata.getId(), { + pluginMetadata, + lifecycle, + }); }, }); diff --git a/packages/backend-app-api/src/services/implementations/database/index.ts b/packages/backend-app-api/src/services/implementations/database/index.ts index 0045630e15..d676c8013e 100644 --- a/packages/backend-app-api/src/services/implementations/database/index.ts +++ b/packages/backend-app-api/src/services/implementations/database/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { databaseFactory } from './databaseFactory'; +export { databaseServiceFactory } from './databaseServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/discovery/discoveryFactory.ts b/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts similarity index 93% rename from packages/backend-app-api/src/services/implementations/discovery/discoveryFactory.ts rename to packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts index 51ea3c881d..6c0da8521d 100644 --- a/packages/backend-app-api/src/services/implementations/discovery/discoveryFactory.ts +++ b/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-plugin-api'; /** @public */ -export const discoveryFactory = createServiceFactory({ +export const discoveryServiceFactory = createServiceFactory({ service: coreServices.discovery, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/services/implementations/discovery/index.ts b/packages/backend-app-api/src/services/implementations/discovery/index.ts index 182b9f6cd2..7b3b9816b1 100644 --- a/packages/backend-app-api/src/services/implementations/discovery/index.ts +++ b/packages/backend-app-api/src/services/implementations/discovery/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { discoveryFactory } from './discoveryFactory'; +export { discoveryServiceFactory } from './discoveryServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts similarity index 85% rename from packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.test.ts rename to packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts index 150bd628f5..e52b4c9a36 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts @@ -14,19 +14,12 @@ * limitations under the License. */ -import { - HttpRouterService, - ServiceFactory, -} from '@backstage/backend-plugin-api'; -import { httpRouterFactory } from './httpRouterFactory'; +import { httpRouterServiceFactory } from './httpRouterServiceFactory'; describe('httpRouterFactory', () => { it('should register plugin paths', async () => { const rootHttpRouter = { use: jest.fn() }; - const factory = httpRouterFactory() as Exclude< - ServiceFactory<HttpRouterService>, - { scope: 'root' } - >; + const factory = httpRouterServiceFactory() as any; const handler1 = () => {}; const router1 = await factory.factory( @@ -55,9 +48,9 @@ describe('httpRouterFactory', () => { it('should use custom path generator', async () => { const rootHttpRouter = { use: jest.fn() }; - const factory = httpRouterFactory({ + const factory = httpRouterServiceFactory({ getPath: id => `/some/${id}/path`, - }) as Exclude<ServiceFactory<HttpRouterService>, { scope: 'root' }>; + }) as any; const handler1 = () => {}; const router1 = await factory.factory( diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts similarity index 93% rename from packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts rename to packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts index 1d52af3b76..bbd54b7ade 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts @@ -27,11 +27,11 @@ export interface HttpRouterFactoryOptions { /** * A callback used to generate the path for each plugin, defaults to `/api/{pluginId}`. */ - getPath(pluginId: string): string; + getPath?(pluginId: string): string; } /** @public */ -export const httpRouterFactory = createServiceFactory( +export const httpRouterServiceFactory = createServiceFactory( (options?: HttpRouterFactoryOptions) => ({ service: coreServices.httpRouter, deps: { diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts index f2db660414..9dfb01c5be 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { httpRouterFactory } from './httpRouterFactory'; -export type { HttpRouterFactoryOptions } from './httpRouterFactory'; +export { httpRouterServiceFactory } from './httpRouterServiceFactory'; +export type { HttpRouterFactoryOptions } from './httpRouterServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/identity/identityFactory.ts b/packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts similarity index 95% rename from packages/backend-app-api/src/services/implementations/identity/identityFactory.ts rename to packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts index 0031979390..58da1c37d5 100644 --- a/packages/backend-app-api/src/services/implementations/identity/identityFactory.ts +++ b/packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts @@ -34,7 +34,7 @@ export type IdentityFactoryOptions = { }; /** @public */ -export const identityFactory = createServiceFactory( +export const identityServiceFactory = createServiceFactory( (options?: IdentityFactoryOptions) => ({ service: coreServices.identity, deps: { diff --git a/packages/backend-app-api/src/services/implementations/identity/index.ts b/packages/backend-app-api/src/services/implementations/identity/index.ts index 9382232620..bf2f4436dd 100644 --- a/packages/backend-app-api/src/services/implementations/identity/index.ts +++ b/packages/backend-app-api/src/services/implementations/identity/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { identityFactory } from './identityFactory'; -export type { IdentityFactoryOptions } from './identityFactory'; +export { identityServiceFactory } from './identityServiceFactory'; +export type { IdentityFactoryOptions } from './identityServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycle/index.ts b/packages/backend-app-api/src/services/implementations/lifecycle/index.ts index cfca37a162..8dac4c26b4 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycle/index.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycle/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { lifecycleFactory } from './lifecycleFactory'; +export { lifecycleServiceFactory } from './lifecycleServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts b/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts similarity index 76% rename from packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts rename to packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts index b2f3a9f70b..79658c0d42 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts @@ -17,12 +17,14 @@ import { createServiceFactory, coreServices, LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, } from '@backstage/backend-plugin-api'; /** * Allows plugins to register shutdown hooks that are run when the process is about to exit. - * @public */ -export const lifecycleFactory = createServiceFactory({ + * @public + */ +export const lifecycleServiceFactory = createServiceFactory({ service: coreServices.lifecycle, deps: { logger: coreServices.logger, @@ -32,11 +34,12 @@ export const lifecycleFactory = createServiceFactory({ async factory({ rootLifecycle, logger, pluginMetadata }) { const plugin = pluginMetadata.getId(); return { - addShutdownHook(options: LifecycleServiceShutdownHook): void { - rootLifecycle.addShutdownHook({ - ...options, - - logger: options.logger?.child({ plugin }) ?? logger, + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + rootLifecycle.addShutdownHook(hook, { + logger: options?.logger?.child({ plugin }) ?? logger, }); }, }; diff --git a/packages/backend-app-api/src/services/implementations/logger/index.ts b/packages/backend-app-api/src/services/implementations/logger/index.ts index acef1b178f..ca52bf5193 100644 --- a/packages/backend-app-api/src/services/implementations/logger/index.ts +++ b/packages/backend-app-api/src/services/implementations/logger/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { loggerFactory } from './loggerFactory'; +export { loggerServiceFactory } from './loggerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/logger/loggerFactory.ts b/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts similarity index 94% rename from packages/backend-app-api/src/services/implementations/logger/loggerFactory.ts rename to packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts index 60c91d0ee6..d9158529ca 100644 --- a/packages/backend-app-api/src/services/implementations/logger/loggerFactory.ts +++ b/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts @@ -20,7 +20,7 @@ import { } from '@backstage/backend-plugin-api'; /** @public */ -export const loggerFactory = createServiceFactory({ +export const loggerServiceFactory = createServiceFactory({ service: coreServices.logger, deps: { rootLogger: coreServices.rootLogger, diff --git a/packages/backend-app-api/src/services/implementations/permissions/index.ts b/packages/backend-app-api/src/services/implementations/permissions/index.ts index 29a6aa237a..781dda31a0 100644 --- a/packages/backend-app-api/src/services/implementations/permissions/index.ts +++ b/packages/backend-app-api/src/services/implementations/permissions/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { permissionsFactory } from './permissionsFactory'; +export { permissionsServiceFactory } from './permissionsServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/permissions/permissionsFactory.ts b/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts similarity index 94% rename from packages/backend-app-api/src/services/implementations/permissions/permissionsFactory.ts rename to packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts index 4eb05e3e4c..38cba73eda 100644 --- a/packages/backend-app-api/src/services/implementations/permissions/permissionsFactory.ts +++ b/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts @@ -21,7 +21,7 @@ import { import { ServerPermissionClient } from '@backstage/plugin-permission-node'; /** @public */ -export const permissionsFactory = createServiceFactory({ +export const permissionsServiceFactory = createServiceFactory({ service: coreServices.permissions, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts index e24662df05..8437399fec 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts @@ -15,10 +15,10 @@ */ export { - rootHttpRouterFactory, + rootHttpRouterServiceFactory, type RootHttpRouterFactoryOptions, - type RootHttpRouterConfigureOptions, -} from './rootHttpRouterFactory'; + type RootHttpRouterConfigureContext, +} from './rootHttpRouterServiceFactory'; export { DefaultRootHttpRouter, type DefaultRootHttpRouterOptions, diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts similarity index 55% rename from packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts rename to packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts index 58f8cc2db1..5eaf8aa152 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -32,7 +32,7 @@ import { DefaultRootHttpRouter } from './DefaultRootHttpRouter'; /** * @public */ -export interface RootHttpRouterConfigureOptions { +export interface RootHttpRouterConfigureContext { app: Express; middleware: MiddlewareFactory; routes: RequestHandler; @@ -51,14 +51,11 @@ export type RootHttpRouterFactoryOptions = { */ indexPath?: string | false; - configure?(options: RootHttpRouterConfigureOptions): void; + configure?(context: RootHttpRouterConfigureContext): void; }; -function defaultConfigure({ - app, - routes, - middleware, -}: RootHttpRouterConfigureOptions) { +function defaultConfigure(context: RootHttpRouterConfigureContext) { + const { app, routes, middleware } = context; app.use(middleware.helmet()); app.use(middleware.cors()); app.use(middleware.compression()); @@ -69,50 +66,42 @@ function defaultConfigure({ } /** @public */ -export const rootHttpRouterFactory = createServiceFactory({ - service: coreServices.rootHttpRouter, - deps: { - config: coreServices.config, - rootLogger: coreServices.rootLogger, - lifecycle: coreServices.rootLifecycle, - }, - async factory( - { config, rootLogger, lifecycle }, - { - indexPath, - configure = defaultConfigure, - }: RootHttpRouterFactoryOptions = {}, - ) { - const logger = rootLogger.child({ service: 'rootHttpRouter' }); - const app = express(); +export const rootHttpRouterServiceFactory = createServiceFactory( + (options?: RootHttpRouterFactoryOptions) => ({ + service: coreServices.rootHttpRouter, + deps: { + config: coreServices.config, + rootLogger: coreServices.rootLogger, + lifecycle: coreServices.rootLifecycle, + }, + async factory({ config, rootLogger, lifecycle }) { + const { indexPath, configure = defaultConfigure } = options ?? {}; + const logger = rootLogger.child({ service: 'rootHttpRouter' }); + const app = express(); - const router = DefaultRootHttpRouter.create({ indexPath }); - const middleware = MiddlewareFactory.create({ config, logger }); + const router = DefaultRootHttpRouter.create({ indexPath }); + const middleware = MiddlewareFactory.create({ config, logger }); - configure({ - app, - routes: router.handler(), - middleware, - config, - logger, - lifecycle, - }); + configure({ + app, + routes: router.handler(), + middleware, + config, + logger, + lifecycle, + }); - const server = await createHttpServer( - app, - readHttpServerOptions(config.getOptionalConfig('backend')), - { logger }, - ); + const server = await createHttpServer( + app, + readHttpServerOptions(config.getOptionalConfig('backend')), + { logger }, + ); - lifecycle.addShutdownHook({ - async fn() { - await server.stop(); - }, - logger, - }); + lifecycle.addShutdownHook(() => server.stop()); - await server.start(); + await server.start(); - return router; - }, -}); + return router; + }, + }), +); diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts index 8adefbfe84..86589cd23e 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { rootLifecycleFactory } from './rootLifecycleFactory'; +export { rootLifecycleServiceFactory } from './rootLifecycleServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts similarity index 73% rename from packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts rename to packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts index fc1c266775..8d2b2af3d5 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts @@ -15,17 +15,13 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { BackendLifecycleImpl } from './rootLifecycleFactory'; +import { BackendLifecycleImpl } from './rootLifecycleServiceFactory'; describe('lifecycleService', () => { it('should execute registered shutdown hook', async () => { const service = new BackendLifecycleImpl(getVoidLogger()); const hook = jest.fn(); - service.addShutdownHook({ - fn: async () => { - hook(); - }, - }); + service.addShutdownHook(() => hook()); // should not execute the hook more than once. await service.shutdown(); await service.shutdown(); @@ -35,10 +31,16 @@ describe('lifecycleService', () => { it('should not throw errors', async () => { const service = new BackendLifecycleImpl(getVoidLogger()); - service.addShutdownHook({ - fn: async () => { - throw new Error('oh no'); - }, + service.addShutdownHook(() => { + throw new Error('oh no'); + }); + await expect(service.shutdown()).resolves.toBeUndefined(); + }); + + it('should not throw async errors', async () => { + const service = new BackendLifecycleImpl(getVoidLogger()); + service.addShutdownHook(async () => { + throw new Error('oh no'); }); await expect(service.shutdown()).resolves.toBeUndefined(); }); diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts similarity index 71% rename from packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts rename to packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts index f451c38f20..f379c2ec52 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts @@ -18,21 +18,25 @@ import { createServiceFactory, coreServices, LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, RootLifecycleService, LoggerService, } from '@backstage/backend-plugin-api'; -const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit']; export class BackendLifecycleImpl implements RootLifecycleService { - constructor(private readonly logger: LoggerService) { - CALLBACKS.map(signal => process.on(signal, () => this.shutdown())); - } + constructor(private readonly logger: LoggerService) {} #isCalled = false; - #shutdownTasks: Array<LifecycleServiceShutdownHook> = []; + #shutdownTasks: Array<{ + hook: LifecycleServiceShutdownHook; + options?: LifecycleServiceShutdownOptions; + }> = []; - addShutdownHook(options: LifecycleServiceShutdownHook): void { - this.#shutdownTasks.push(options); + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + this.#shutdownTasks.push({ hook, options }); } async shutdown(): Promise<void> { @@ -43,10 +47,10 @@ export class BackendLifecycleImpl implements RootLifecycleService { this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( - this.#shutdownTasks.map(async hook => { - const { logger = this.logger } = hook; + this.#shutdownTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; try { - await hook.fn(); + await hook(); logger.info(`Shutdown hook succeeded`); } catch (error) { logger.error(`Shutdown hook failed, ${error}`); @@ -58,8 +62,10 @@ export class BackendLifecycleImpl implements RootLifecycleService { /** * Allows plugins to register shutdown hooks that are run when the process is about to exit. - * @public */ -export const rootLifecycleFactory = createServiceFactory({ + * + * @public + */ +export const rootLifecycleServiceFactory = createServiceFactory({ service: coreServices.rootLifecycle, deps: { logger: coreServices.rootLogger, diff --git a/packages/backend-app-api/src/services/implementations/rootLogger/index.ts b/packages/backend-app-api/src/services/implementations/rootLogger/index.ts index ce5816e6fa..366cd53163 100644 --- a/packages/backend-app-api/src/services/implementations/rootLogger/index.ts +++ b/packages/backend-app-api/src/services/implementations/rootLogger/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { rootLoggerFactory } from './rootLoggerFactory'; +export { rootLoggerServiceFactory } from './rootLoggerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerFactory.ts b/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts similarity index 96% rename from packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerFactory.ts rename to packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts index 1c2214b98c..11bbd58050 100644 --- a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts @@ -23,7 +23,7 @@ import { transports, format } from 'winston'; import { createConfigSecretEnumerator } from '../../../config'; /** @public */ -export const rootLoggerFactory = createServiceFactory({ +export const rootLoggerServiceFactory = createServiceFactory({ service: coreServices.rootLogger, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/services/implementations/scheduler/index.ts b/packages/backend-app-api/src/services/implementations/scheduler/index.ts index 8b84906b5b..8b67006a4d 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/index.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { schedulerFactory } from './schedulerFactory'; +export { schedulerServiceFactory } from './schedulerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts new file mode 100644 index 0000000000..7af9de44da --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { schedulerServiceFactory } from './schedulerServiceFactory'; + +describe('schedulerFactory', () => { + it('creates sidecar database features', async () => { + expect.assertions(3); + + const subject = schedulerServiceFactory(); + + const plugin = createBackendPlugin({ + pluginId: 'example', + register(reg) { + reg.registerInit({ + deps: { + scheduler: subject.service, + database: coreServices.database, + }, + init: async ({ scheduler, database }) => { + await scheduler.scheduleTask({ + id: 'task1', + timeout: { seconds: 1 }, + frequency: { seconds: 1 }, + fn: async () => {}, + }); + + const client = await database.getClient(); + await expect( + client.from('backstage_backend_tasks__tasks').count(), + ).resolves.toEqual([{ 'count(*)': 1 }]); + await expect( + client.from('backstage_backend_tasks__knex_migrations').count(), + ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); + await expect( + client + .from('backstage_backend_tasks__knex_migrations_lock') + .count(), + ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); + }, + }); + }, + }); + + await startTestBackend({ + features: [plugin()], + services: [subject], + }); + }); +}); diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts similarity index 95% rename from packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.ts rename to packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts index 458c5acfd6..4dfe567ccc 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts @@ -22,7 +22,7 @@ import { import { TaskScheduler } from '@backstage/backend-tasks'; /** @public */ -export const schedulerFactory = createServiceFactory({ +export const schedulerServiceFactory = createServiceFactory({ service: coreServices.scheduler, deps: { plugin: coreServices.pluginMetadata, diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/index.ts b/packages/backend-app-api/src/services/implementations/tokenManager/index.ts index c0685079c4..3ae3d45d2f 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManager/index.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManager/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { tokenManagerFactory } from './tokenManagerFactory'; +export { tokenManagerServiceFactory } from './tokenManagerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.test.ts b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts similarity index 81% rename from packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.test.ts rename to packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts index abc62fff69..aca5211e2e 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts @@ -14,22 +14,15 @@ * limitations under the License. */ -import { - LoggerService, - ServiceFactory, - TokenManagerService, -} from '@backstage/backend-plugin-api'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { ConfigReader } from '@backstage/config'; -import { tokenManagerFactory } from './tokenManagerFactory'; +import { tokenManagerServiceFactory } from './tokenManagerServiceFactory'; describe('tokenManagerFactory', () => { it('should create managers that can share tokens in development', async () => { (process.env as { NODE_ENV?: string }).NODE_ENV = 'development'; - const factory = tokenManagerFactory() as Exclude< - ServiceFactory<TokenManagerService>, - { scope: 'root' } - >; + const factory = tokenManagerServiceFactory() as any; const deps = { config: new ConfigReader({}), logger: { warn() {} } as unknown as LoggerService, diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.ts b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts similarity index 94% rename from packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.ts rename to packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts index ae0b402eec..b3726176b6 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts @@ -21,7 +21,7 @@ import { import { ServerTokenManager } from '@backstage/backend-common'; /** @public */ -export const tokenManagerFactory = createServiceFactory({ +export const tokenManagerServiceFactory = createServiceFactory({ service: coreServices.tokenManager, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/services/implementations/urlReader/index.ts b/packages/backend-app-api/src/services/implementations/urlReader/index.ts index 5da0ef4049..6a4b9f65be 100644 --- a/packages/backend-app-api/src/services/implementations/urlReader/index.ts +++ b/packages/backend-app-api/src/services/implementations/urlReader/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { urlReaderFactory } from './urlReaderFactory'; +export { urlReaderServiceFactory } from './urlReaderServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderFactory.ts b/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts similarity index 94% rename from packages/backend-app-api/src/services/implementations/urlReader/urlReaderFactory.ts rename to packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts index 1b52ce9604..83e594aa0b 100644 --- a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderFactory.ts +++ b/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-plugin-api'; /** @public */ -export const urlReaderFactory = createServiceFactory({ +export const urlReaderServiceFactory = createServiceFactory({ service: coreServices.urlReader, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index ce17398a79..2a8fa1e720 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -20,16 +20,19 @@ import { coreServices, ServiceRef, } from '@backstage/backend-plugin-api'; -import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleFactory'; +import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleServiceFactory'; import { BackendRegisterInit, EnumerableServiceHolder, ServiceOrExtensionPoint, } from './types'; +// Direct internal import to avoid duplication +// eslint-disable-next-line @backstage/no-forbidden-package-imports +import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; export class BackendInitializer { - #started = false; - #features = new Map<BackendFeature, unknown>(); + #startPromise?: Promise<void>; + #features = new Array<InternalBackendFeature>(); #registerInits = new Array<BackendRegisterInit>(); #extensionPoints = new Map<ExtensionPoint<unknown>, unknown>(); #serviceHolder: EnumerableServiceHolder; @@ -74,19 +77,52 @@ export class BackendInitializer { return Object.fromEntries(result); } - add<TOptions>(feature: BackendFeature, options?: TOptions) { - if (this.#started) { + add(feature: BackendFeature) { + if (this.#startPromise) { throw new Error('feature can not be added after the backend has started'); } - this.#features.set(feature, options); + if (feature.$$type !== '@backstage/BackendFeature') { + throw new Error( + `Failed to add feature, invalid type '${feature.$$type}'`, + ); + } + const internalFeature = feature as InternalBackendFeature; + if (internalFeature.version !== 'v1') { + throw new Error( + `Failed to add feature, invalid version '${internalFeature.version}'`, + ); + } + this.#features.push(internalFeature); } async start(): Promise<void> { - if (this.#started) { + if (this.#startPromise) { throw new Error('Backend has already started'); } - this.#started = true; + const exitHandler = async () => { + process.removeListener('SIGTERM', exitHandler); + process.removeListener('SIGINT', exitHandler); + process.removeListener('beforeExit', exitHandler); + + try { + await this.stop(); + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } + }; + + process.addListener('SIGTERM', exitHandler); + process.addListener('SIGINT', exitHandler); + process.addListener('beforeExit', exitHandler); + + this.#startPromise = this.#doStart(); + await this.#startPromise; + } + + async #doStart(): Promise<void> { // Initialize all root scoped services for (const ref of this.#serviceHolder.getServiceRefs()) { if (ref.scope === 'root') { @@ -95,50 +131,39 @@ export class BackendInitializer { } // Initialize all features - for (const [feature] of this.#features) { - const provides = new Set<ExtensionPoint<unknown>>(); + for (const feature of this.#features) { + for (const r of feature.getRegistrations()) { + const provides = new Set<ExtensionPoint<unknown>>(); - let registerInit: BackendRegisterInit | undefined = undefined; + if (r.type === 'plugin') { + for (const [extRef, extImpl] of r.extensionPoints) { + if (this.#extensionPoints.has(extRef)) { + throw new Error( + `ExtensionPoint with ID '${extRef.id}' is already registered`, + ); + } + this.#extensionPoints.set(extRef, extImpl); + provides.add(extRef); + } + } - feature.register({ - registerExtensionPoint: (extensionPointRef, impl) => { - if (registerInit) { - throw new Error('registerExtensionPoint called after registerInit'); - } - if (this.#extensionPoints.has(extensionPointRef)) { - throw new Error(`API ${extensionPointRef.id} already registered`); - } - this.#extensionPoints.set(extensionPointRef, impl); - provides.add(extensionPointRef); - }, - registerInit: registerOptions => { - if (registerInit) { - throw new Error('registerInit must only be called once'); - } - registerInit = { - id: feature.id, - provides, - consumes: new Set(Object.values(registerOptions.deps)), - deps: registerOptions.deps, - init: registerOptions.init as BackendRegisterInit['init'], - }; - }, - }); - - if (!registerInit) { - throw new Error( - `registerInit was not called by register in ${feature.id}`, - ); + this.#registerInits.push({ + id: r.type === 'plugin' ? r.pluginId : `${r.pluginId}.${r.moduleId}`, + provides, + consumes: new Set(Object.values(r.init.deps)), + init: r.init, + }); } - - this.#registerInits.push(registerInit); } const orderedRegisterResults = this.#resolveInitOrder(this.#registerInits); for (const registerInit of orderedRegisterResults) { - const deps = await this.#getInitDeps(registerInit.deps, registerInit.id); - await registerInit.init(deps); + const deps = await this.#getInitDeps( + registerInit.init.deps, + registerInit.id, + ); + await registerInit.init.func(deps); } } @@ -177,9 +202,10 @@ export class BackendInitializer { } async stop(): Promise<void> { - if (!this.#started) { + if (!this.#startPromise) { return; } + await this.#startPromise; const lifecycleService = await this.#serviceHolder.get( coreServices.rootLifecycle, diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index 68dff3f7c3..b5b55f67cf 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -18,36 +18,66 @@ import { ServiceFactory, ServiceRef, coreServices, + createServiceFactory, } from '@backstage/backend-plugin-api'; import { stringifyError } from '@backstage/errors'; import { EnumerableServiceHolder } from './types'; +// Direct internal import to avoid duplication +// eslint-disable-next-line @backstage/no-forbidden-package-imports +import { InternalServiceFactory } from '@backstage/backend-plugin-api/src/services/system/types'; /** * Keep in sync with `@backstage/backend-plugin-api/src/services/system/types.ts` * @internal */ -export type InternalServiceRef<T> = ServiceRef<T> & { +export type InternalServiceRef = ServiceRef<unknown> & { __defaultFactory?: ( - service: ServiceRef<T>, - ) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>; + service: ServiceRef<unknown>, + ) => Promise<ServiceFactory | (() => ServiceFactory)>; }; +function toInternalServiceFactory<TService, TScope extends 'plugin' | 'root'>( + factory: ServiceFactory<TService, TScope>, +): InternalServiceFactory<TService, TScope> { + const f = factory as InternalServiceFactory<TService, TScope>; + if (f.$$type !== '@backstage/ServiceFactory') { + throw new Error(`Invalid service factory, bad type '${f.$$type}'`); + } + if (f.version !== 'v1') { + throw new Error(`Invalid service factory, bad version '${f.version}'`); + } + return f; +} + +const pluginMetadataServiceFactory = createServiceFactory( + (options: { pluginId: string }) => ({ + service: coreServices.pluginMetadata, + deps: {}, + factory: async () => ({ getId: () => options.pluginId }), + }), +); + export class ServiceRegistry implements EnumerableServiceHolder { - readonly #providedFactories: Map<string, ServiceFactory>; - readonly #loadedDefaultFactories: Map<Function, Promise<ServiceFactory>>; + readonly #providedFactories: Map<string, InternalServiceFactory>; + readonly #loadedDefaultFactories: Map< + Function, + Promise<InternalServiceFactory> + >; readonly #implementations: Map< - ServiceFactory, + InternalServiceFactory, { context: Promise<unknown>; byPlugin: Map<string, Promise<unknown>>; } >; readonly #rootServiceImplementations = new Map< - ServiceFactory, + InternalServiceFactory, Promise<unknown> >(); - constructor(factories: Array<ServiceFactory<unknown>>) { - this.#providedFactories = new Map(factories.map(f => [f.service.id, f])); + constructor(factories: Array<ServiceFactory>) { + this.#providedFactories = new Map( + factories.map(sf => [sf.service.id, toInternalServiceFactory(sf)]), + ); this.#loadedDefaultFactories = new Map(); this.#implementations = new Map(); } @@ -55,23 +85,19 @@ export class ServiceRegistry implements EnumerableServiceHolder { #resolveFactory( ref: ServiceRef<unknown>, pluginId: string, - ): Promise<ServiceFactory> | undefined { + ): Promise<InternalServiceFactory> | undefined { // Special case handling of the plugin metadata service, generating a custom factory for it each time if (ref.id === coreServices.pluginMetadata.id) { - return Promise.resolve< - ServiceFactory<typeof coreServices.pluginMetadata.T> - >({ - scope: 'plugin', - service: coreServices.pluginMetadata, - deps: {}, - factory: async () => ({ getId: () => pluginId }), - }); + return Promise.resolve( + toInternalServiceFactory(pluginMetadataServiceFactory({ pluginId })), + ); } - let resolvedFactory: Promise<ServiceFactory> | ServiceFactory | undefined = - this.#providedFactories.get(ref.id); - const { __defaultFactory: defaultFactory } = - ref as InternalServiceRef<unknown>; + let resolvedFactory: + | Promise<InternalServiceFactory> + | InternalServiceFactory + | undefined = this.#providedFactories.get(ref.id); + const { __defaultFactory: defaultFactory } = ref as InternalServiceRef; if (!resolvedFactory && !defaultFactory) { return undefined; } @@ -82,8 +108,8 @@ export class ServiceRegistry implements EnumerableServiceHolder { loadedFactory = Promise.resolve() .then(() => defaultFactory!(ref)) .then(f => - typeof f === 'function' ? f() : f, - ) as Promise<ServiceFactory>; + toInternalServiceFactory(typeof f === 'function' ? f() : f), + ); this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); } resolvedFactory = loadedFactory.catch(error => { @@ -100,7 +126,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { return Promise.resolve(resolvedFactory); } - #checkForMissingDeps(factory: ServiceFactory, pluginId: string) { + #checkForMissingDeps(factory: InternalServiceFactory, pluginId: string) { const missingDeps = Object.values(factory.deps).filter(ref => { if (ref.id === coreServices.pluginMetadata.id) { return false; @@ -109,7 +135,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { return false; } - return !(ref as InternalServiceRef<unknown>).__defaultFactory; + return !(ref as InternalServiceRef).__defaultFactory; }); if (missingDeps.length) { @@ -126,7 +152,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { get<T>(ref: ServiceRef<T>, pluginId: string): Promise<T> | undefined { return this.#resolveFactory(ref, pluginId)?.then(factory => { - if (factory.scope === 'root') { + if (factory.service.scope === 'root') { let existing = this.#rootServiceImplementations.get(factory); if (!existing) { this.#checkForMissingDeps(factory, pluginId); @@ -143,7 +169,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { } existing = Promise.all(rootDeps).then(entries => - factory.factory(Object.fromEntries(entries)), + factory.factory(Object.fromEntries(entries), undefined), ); this.#rootServiceImplementations.set(factory, existing); } diff --git a/packages/backend-app-api/src/wiring/index.ts b/packages/backend-app-api/src/wiring/index.ts index 6e0fb272ae..00e4a9d4b4 100644 --- a/packages/backend-app-api/src/wiring/index.ts +++ b/packages/backend-app-api/src/wiring/index.ts @@ -14,9 +14,5 @@ * limitations under the License. */ -export type { - Backend, - CreateSpecializedBackendOptions, - ServiceOrExtensionPoint, -} from './types'; +export type { Backend, CreateSpecializedBackendOptions } from './types'; export { createSpecializedBackend } from './createSpecializedBackend'; diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index d3c4fc48c7..2a91bf46b9 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -34,8 +34,10 @@ export interface BackendRegisterInit { id: string; consumes: Set<ServiceOrExtensionPoint>; provides: Set<ServiceOrExtensionPoint>; - deps: { [name: string]: ServiceOrExtensionPoint }; - init: (deps: { [name: string]: unknown }) => Promise<void>; + init: { + deps: { [name: string]: ServiceOrExtensionPoint }; + func: (deps: { [name: string]: unknown }) => Promise<void>; + }; } /** diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 637126db56..dc07ac0614 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,88 @@ # @backstage/backend-common +## 0.18.3-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-app-api@0.4.1-next.0 + - @backstage/backend-dev-utils@0.1.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.18.2 + +### Patch Changes + +- 5febb216fe: **BREAKING**: The `CacheClient` interface must now also implement the `withOptions` method. The `.get()` method has also received a type parameter that helps ensure that `undefined` in the event of a cache miss is handled. + + Added a `cacheToPluginCacheManager` helper that converts a `CacheService` into a legacy `PluginCacheManager` instance. + +- 5febb216fe: Updated to match the new `CacheService` interface. +- e716946103: Updated usage of the lifecycle service. +- d31d8e00b3: Updated to work with the new `type: 'pem'` with `createHttpServer` from `@backstage/backend-app-api` +- 0ff03319be: Updated usage of `createBackendPlugin`. +- f60cca9da1: The `DatabaseManager.forPlugin` method now accepts additional service dependencies. There is no need to update existing code to pass these dependencies. +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/backend-app-api@0.4.0 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-dev-utils@0.1.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.18.2-next.2 + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- d31d8e00b3: Updated to work with the new `type: 'pem'` with `createHttpServer` from `@backstage/backend-app-api` +- 0ff03319be: Updated usage of `createBackendPlugin`. +- f60cca9da1: The `DatabaseManager.forPlugin` method now accepts additional service dependencies. There is no need to update existing code to pass these dependencies. +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-dev-utils@0.1.0-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.18.2-next.1 + +### Patch Changes + +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-app-api@0.3.2-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.18.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.3.2-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.18.0 ### Minor Changes diff --git a/packages/backend-common/README.md b/packages/backend-common/README.md index a1ebf18841..578fbc0830 100644 --- a/packages/backend-common/README.md +++ b/packages/backend-common/README.md @@ -36,4 +36,4 @@ app.listen(PORT, () => { ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/backend-common/alpha-api-report.md b/packages/backend-common/alpha-api-report.md new file mode 100644 index 0000000000..9d60b68b6b --- /dev/null +++ b/packages/backend-common/alpha-api-report.md @@ -0,0 +1,32 @@ +## API Report File for "@backstage/backend-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Duration } from 'luxon'; + +// @alpha +export interface Context { + readonly abortSignal: AbortSignal; + readonly deadline: Date | undefined; + value<T = unknown>(key: string): T | undefined; +} + +// @alpha +export class Contexts { + static root(): Context; + static withAbort( + parentCtx: Context, + source: AbortController | AbortSignal, + ): Context; + static withTimeoutDuration(parentCtx: Context, timeout: Duration): Context; + static withTimeoutMillis(parentCtx: Context, timeout: number): Context; + static withValue( + parentCtx: Context, + key: string, + value: unknown | ((previous: unknown | undefined) => unknown), + ): Context; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index b03d6ec866..1cde27877b 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -13,14 +13,13 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketCloudIntegration } from '@backstage/integration'; import { BitbucketIntegration } from '@backstage/integration'; import { BitbucketServerIntegration } from '@backstage/integration'; -import { CacheClient } from '@backstage/backend-plugin-api'; -import { CacheClientOptions } from '@backstage/backend-plugin-api'; -import { CacheClientSetOptions } from '@backstage/backend-plugin-api'; +import { CacheService as CacheClient } from '@backstage/backend-plugin-api'; +import { CacheServiceOptions as CacheClientOptions } from '@backstage/backend-plugin-api'; +import { CacheServiceSetOptions as CacheClientSetOptions } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { ConfigService } from '@backstage/backend-plugin-api'; import cors from 'cors'; import Docker from 'dockerode'; -import { Duration } from 'luxon'; import { ErrorRequestHandler } from 'express'; import express from 'express'; import { GerritIntegration } from '@backstage/integration'; @@ -32,14 +31,15 @@ import { IdentityService } from '@backstage/backend-plugin-api'; import { isChildPath } from '@backstage/cli-common'; import { Knex } from 'knex'; import { KubeConfig } from '@kubernetes/client-node'; +import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; import { Logger } from 'winston'; import { LoggerService } from '@backstage/backend-plugin-api'; import { MergeResult } from 'isomorphic-git'; import { PermissionsService } from '@backstage/backend-plugin-api'; -import { CacheService as PluginCacheManager } from '@backstage/backend-plugin-api'; import { DatabaseService as PluginDatabaseManager } from '@backstage/backend-plugin-api'; import { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api'; +import { PluginMetadataService } from '@backstage/backend-plugin-api'; import { PushResult } from 'isomorphic-git'; import { Readable } from 'stream'; import { ReadCommitResult } from 'isomorphic-git'; @@ -197,6 +197,11 @@ export type CacheManagerOptions = { onError?: (err: Error) => void; }; +// @public (undocumented) +export function cacheToPluginCacheManager( + cache: CacheClient, +): PluginCacheManager; + // @public export const coloredFormat: winston.Logform.Format; @@ -205,33 +210,14 @@ export interface ContainerRunner { runContainer(opts: RunContainerOptions): Promise<void>; } -// @alpha -export interface Context { - readonly abortSignal: AbortSignal; - readonly deadline: Date | undefined; - value<T = unknown>(key: string): T | undefined; -} - -// @alpha -export class Contexts { - static root(): Context; - static withAbort( - parentCtx: Context, - source: AbortController | AbortSignal, - ): Context; - static withTimeoutDuration(parentCtx: Context, timeout: Duration): Context; - static withTimeoutMillis(parentCtx: Context, timeout: number): Context; - static withValue( - parentCtx: Context, - key: string, - value: unknown | ((previous: unknown | undefined) => unknown), - ): Context; -} - // @public export function createDatabaseClient( dbConfig: Config, overrides?: Partial<Knex.Config>, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ): Knex<any, any[]>; // @public @@ -252,7 +238,13 @@ export function createStatusCheckRouter(options: { // @public export class DatabaseManager { - forPlugin(pluginId: string): PluginDatabaseManager; + forPlugin( + pluginId: string, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): PluginDatabaseManager; static fromConfig( config: Config, options?: DatabaseManagerOptions, @@ -516,7 +508,7 @@ export const legacyPlugin: ( default: LegacyCreateRouter< TransformedEnv< { - cache: PluginCacheManager; + cache: CacheClient; config: ConfigService; database: PluginDatabaseManager; discovery: PluginEndpointDiscovery; @@ -529,6 +521,7 @@ export const legacyPlugin: ( }, { logger: (log: LoggerService) => Logger; + cache: (cache: CacheClient) => PluginCacheManager; } > >; @@ -569,7 +562,11 @@ export function makeLegacyPlugin< // @public export function notFoundHandler(): RequestHandler; -export { PluginCacheManager }; +// @public (undocumented) +export interface PluginCacheManager { + // (undocumented) + getClient(options?: CacheClientOptions): CacheClient; +} export { PluginDatabaseManager }; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 8dbe82a411..28c11e39e0 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,14 +1,26 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.18.0", + "version": "0.18.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "node-library" @@ -24,7 +36,7 @@ ], "license": "Apache-2.0", "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -35,6 +47,7 @@ }, "dependencies": { "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-dev-utils": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/config": "workspace:^", @@ -45,7 +58,7 @@ "@google-cloud/storage": "^6.0.0", "@keyv/memcache": "^1.3.5", "@keyv/redis": "^2.5.3", - "@kubernetes/client-node": "0.18.0", + "@kubernetes/client-node": "0.18.1", "@manypkg/get-packages": "^1.1.3", "@octokit/rest": "^19.0.3", "@types/cors": "^2.8.6", @@ -119,7 +132,7 @@ "better-sqlite3": "^8.0.0", "http-errors": "^2.0.0", "mock-fs": "^5.1.0", - "msw": "^0.49.0", + "msw": "^1.0.0", "mysql2": "^2.2.5", "recursive-readdir": "^2.2.2", "supertest": "^6.1.3" diff --git a/packages/backend-common/src/alpha.ts b/packages/backend-common/src/alpha.ts new file mode 100644 index 0000000000..e0a5b2ddc2 --- /dev/null +++ b/packages/backend-common/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './context'; diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts index 8a5779550a..245d8687c9 100644 --- a/packages/backend-common/src/cache/CacheClient.test.ts +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -32,7 +32,7 @@ describe('CacheClient', () => { describe('CacheClient.get', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const key = 'somekey'; await sut.get(key); @@ -41,7 +41,7 @@ describe('CacheClient', () => { }); it('calls client with normalized key (very long key)', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const key = 'x'.repeat(251); await sut.get(key); @@ -53,7 +53,7 @@ describe('CacheClient', () => { }); it('rejects on underlying error', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedError = new Error('Some runtime error'); client.get = jest.fn().mockRejectedValue(expectedError); @@ -61,7 +61,7 @@ describe('CacheClient', () => { }); it('resolves what underlying client resolves', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedValue = 'some value'; client.get = jest.fn().mockResolvedValue(expectedValue); @@ -73,7 +73,7 @@ describe('CacheClient', () => { describe('CacheClient.set', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const key = 'somekey'; await sut.set(key, {}); @@ -84,7 +84,7 @@ describe('CacheClient', () => { }); it('passes ttl to client when given', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedTtl = 3600; await sut.set('someKey', {}, { ttl: expectedTtl }); @@ -95,7 +95,7 @@ describe('CacheClient', () => { }); it('rejects on underlying error if configured', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedError = new Error('Some runtime error'); client.set = jest.fn().mockRejectedValue(expectedError); @@ -105,7 +105,7 @@ describe('CacheClient', () => { describe('CacheClient.delete', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const key = 'somekey'; await sut.delete(key); @@ -116,11 +116,35 @@ describe('CacheClient', () => { }); it('rejects on underlying error if configured', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedError = new Error('Some runtime error'); client.delete = jest.fn().mockRejectedValue(expectedError); return expect(sut.delete('someKey')).rejects.toEqual(expectedError); }); }); + + describe('CacheClient.withOptions', () => { + it('merges together options to create a new instance', async () => { + const factory = jest.fn(); + const sut = new DefaultCacheClient(client, factory, { foo: 1 } as any); + expect(factory).not.toHaveBeenCalled(); + + const sutA = await sut.withOptions({}); + expect(factory).toHaveBeenLastCalledWith({ foo: 1 }); + + const sutA2 = await sutA.withOptions({ bar: 2 } as any); + expect(factory).toHaveBeenCalledWith({ foo: 1, bar: 2 }); + + await sutA2.withOptions({ foo: 3 } as any); + expect(factory).toHaveBeenCalledWith({ foo: 3, bar: 2 }); + + // calling .withOptions should not mutate state + const sutB = await sut.withOptions({ foo: 2 } as any); + expect(factory).toHaveBeenLastCalledWith({ foo: 2 }); + + await sutB.withOptions({ bar: 3 } as any); + expect(factory).toHaveBeenLastCalledWith({ foo: 2, bar: 3 }); + }); + }); }); diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 47cb786f14..24ee7ac455 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -15,50 +15,64 @@ */ import { - CacheClient, - CacheClientSetOptions, + CacheService, + CacheServiceOptions, + CacheServiceSetOptions, } from '@backstage/backend-plugin-api'; import { JsonValue } from '@backstage/types'; import { createHash } from 'crypto'; import Keyv from 'keyv'; -export type { - CacheClient, - CacheClientSetOptions, -} from '@backstage/backend-plugin-api'; - -type CacheClientArgs = { - client: Keyv; -}; +export type CacheClientFactory = (options: CacheServiceOptions) => Keyv; /** - * A basic, concrete implementation of the CacheClient, suitable for almost + * A basic, concrete implementation of the CacheService, suitable for almost * all uses in Backstage. */ -export class DefaultCacheClient implements CacheClient { - private readonly client: Keyv; +export class DefaultCacheClient implements CacheService { + #client: Keyv; + #clientFactory: CacheClientFactory; + #options: CacheServiceOptions; - constructor({ client }: CacheClientArgs) { - this.client = client; + constructor( + client: Keyv, + clientFactory: CacheClientFactory, + options: CacheServiceOptions, + ) { + this.#client = client; + this.#clientFactory = clientFactory; + this.#options = options; } - async get(key: string): Promise<JsonValue | undefined> { + async get<TValue extends JsonValue>( + key: string, + ): Promise<TValue | undefined> { const k = this.getNormalizedKey(key); - return await this.client.get(k); + const value = await this.#client.get(k); + return value as TValue | undefined; } async set( key: string, value: JsonValue, - opts: CacheClientSetOptions = {}, + opts: CacheServiceSetOptions = {}, ): Promise<void> { const k = this.getNormalizedKey(key); - await this.client.set(k, value, opts.ttl); + await this.#client.set(k, value, opts.ttl); } async delete(key: string): Promise<void> { const k = this.getNormalizedKey(key); - await this.client.delete(k); + await this.#client.delete(k); + } + + withOptions(options: CacheServiceOptions): CacheService { + const newOptions = { ...this.#options, ...options }; + return new DefaultCacheClient( + this.#clientFactory(newOptions), + this.#clientFactory, + newOptions, + ); } /** diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index 50b775af25..3239be4d0e 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -94,7 +94,7 @@ describe('CacheManager', () => { const client = DefaultCacheClient as jest.Mock; const mockCalls = client.mock.calls.splice(-1); - const realClient = mockCalls[0][0].client as Keyv; + const realClient = mockCalls[0][0] as Keyv; expect(realClient.on).toHaveBeenCalledWith('error', expect.any(Function)); }); @@ -233,7 +233,7 @@ describe('CacheManager', () => { // Retrieve the error handler attached to the cache client. const client = DefaultCacheClient as jest.Mock; const mockCalls = client.mock.calls.splice(-1); - const realClient = mockCalls[0][0].client as Keyv; + const realClient = mockCalls[0][0] as Keyv; const realOnError = realClient.on as jest.Mock; const realHandler = realOnError.mock.calls.splice(-1)[0][1]; @@ -259,7 +259,7 @@ describe('CacheManager', () => { // Retrieve the error handler attached to the cache client. const client = DefaultCacheClient as jest.Mock; const mockCalls = client.mock.calls.splice(-1); - const realClient = mockCalls[0][0].client as Keyv; + const realClient = mockCalls[0][0] as Keyv; const realOnError = realClient.on as jest.Mock; const realHandler = realOnError.mock.calls.splice(-1)[0][1]; diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 4f7f5403a8..12ece42854 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -18,9 +18,13 @@ import { Config } from '@backstage/config'; import Keyv from 'keyv'; import KeyvMemcache from '@keyv/memcache'; import KeyvRedis from '@keyv/redis'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { + CacheService, + CacheServiceOptions, + LoggerService, +} from '@backstage/backend-plugin-api'; import { getRootLogger } from '../logging'; -import { DefaultCacheClient, CacheClient } from './CacheClient'; +import { DefaultCacheClient } from './CacheClient'; import { NoStore } from './NoStore'; import { CacheManagerOptions, PluginCacheManager } from './types'; @@ -99,23 +103,32 @@ export class CacheManager { */ forPlugin(pluginId: string): PluginCacheManager { return { - getClient: (opts = {}): CacheClient => { - const concreteClient = this.getClientWithTtl(pluginId, opts.defaultTtl); + getClient: (defaultOptions = {}) => { + const clientFactory = (options: CacheServiceOptions) => { + const concreteClient = this.getClientWithTtl( + pluginId, + options.defaultTtl, + ); - // Always provide an error handler to avoid stopping the process. - concreteClient.on('error', (err: Error) => { - // In all cases, just log the error. - this.logger.error('Failed to create cache client', err); + // Always provide an error handler to avoid stopping the process. + concreteClient.on('error', (err: Error) => { + // In all cases, just log the error. + this.logger.error('Failed to create cache client', err); - // Invoke any custom error handler if provided. - if (typeof this.errorHandler === 'function') { - this.errorHandler(err); - } - }); + // Invoke any custom error handler if provided. + if (typeof this.errorHandler === 'function') { + this.errorHandler(err); + } + }); - return new DefaultCacheClient({ - client: concreteClient, - }); + return concreteClient; + }; + + return new DefaultCacheClient( + clientFactory(defaultOptions), + clientFactory, + defaultOptions, + ); }, }; } @@ -164,3 +177,12 @@ export class CacheManager { }); } } + +/** @public */ +export function cacheToPluginCacheManager( + cache: CacheService, +): PluginCacheManager { + return { + getClient: (opts: CacheServiceOptions) => cache.withOptions(opts), + }; +} diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts index 26e6c102d8..0187aa45d5 100644 --- a/packages/backend-common/src/cache/index.ts +++ b/packages/backend-common/src/cache/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -export type { CacheClient, CacheClientSetOptions } from './CacheClient'; -export { CacheManager } from './CacheManager'; +export { CacheManager, cacheToPluginCacheManager } from './CacheManager'; export type { + CacheClient, + CacheClientSetOptions, PluginCacheManager, CacheManagerOptions, CacheClientOptions, diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts index eeeafde5b6..93fa0ac77c 100644 --- a/packages/backend-common/src/cache/types.ts +++ b/packages/backend-common/src/cache/types.ts @@ -15,10 +15,15 @@ */ import { LoggerService } from '@backstage/backend-plugin-api'; +import { + CacheService, + CacheServiceOptions, +} from '@backstage/backend-plugin-api'; export type { - CacheService as PluginCacheManager, - CacheClientOptions, + CacheService as CacheClient, + CacheServiceSetOptions as CacheClientSetOptions, + CacheServiceOptions as CacheClientOptions, } from '@backstage/backend-plugin-api'; /** @@ -38,3 +43,10 @@ export type CacheManagerOptions = { */ onError?: (err: Error) => void; }; + +/** + * @public + */ +export interface PluginCacheManager { + getClient(options?: CacheServiceOptions): CacheService; +} diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 5a497fff29..08979a92f8 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -29,7 +29,11 @@ import { } from './connection'; import { PluginDatabaseManager } from './types'; import path from 'path'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { + LifecycleService, + LoggerService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; import { stringifyError } from '@backstage/errors'; /** @@ -94,12 +98,18 @@ export class DatabaseManager { * should be unique as they are used to look up database config overrides under * `backend.database.plugin`. */ - forPlugin(pluginId: string): PluginDatabaseManager { + forPlugin( + pluginId: string, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): PluginDatabaseManager { const _this = this; return { getClient(): Promise<Knex> { - return _this.getDatabase(pluginId); + return _this.getDatabase(pluginId, deps); }, migrations: { skip: false, @@ -307,7 +317,13 @@ export class DatabaseManager { * @returns Promise which resolves to a scoped Knex database client for a * plugin */ - private async getDatabase(pluginId: string): Promise<Knex> { + private async getDatabase( + pluginId: string, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): Promise<Knex> { if (this.databaseCache.has(pluginId)) { return this.databaseCache.get(pluginId)!; } @@ -351,6 +367,7 @@ export class DatabaseManager { const client = createDatabaseClient( pluginConfig, databaseClientOverrides, + deps, ); this.startKeepaliveLoop(pluginId, client); return client; diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 5f487886b4..ff10d349dd 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -22,6 +22,10 @@ import { mergeDatabaseConfig } from './config'; import { DatabaseConnector } from './types'; import { mysqlConnector, pgConnector, sqlite3Connector } from './connectors'; +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; type DatabaseClient = | 'pg' @@ -55,11 +59,15 @@ const ConnectorMapping: Record<DatabaseClient, DatabaseConnector> = { export function createDatabaseClient( dbConfig: Config, overrides?: Partial<Knex.Config>, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ) { const client: DatabaseClient = dbConfig.getString('client'); return ( - ConnectorMapping[client]?.createClient(dbConfig, overrides) ?? + ConnectorMapping[client]?.createClient(dbConfig, overrides, deps) ?? knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides)) ); } diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts index 6426005f0e..3cb0f57de6 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -18,8 +18,13 @@ import { Config } from '@backstage/config'; import { ensureDirSync } from 'fs-extra'; import knexFactory, { Knex } from 'knex'; import path from 'path'; +import { DevDataStore } from '@backstage/backend-dev-utils'; import { mergeDatabaseConfig } from '../config'; import { DatabaseConnector } from '../types'; +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; /** * Creates a knex SQLite3 database connection @@ -30,22 +35,63 @@ import { DatabaseConnector } from '../types'; export function createSqliteDatabaseClient( dbConfig: Config, overrides?: Knex.Config, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ) { const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides); + const connConfig = knexConfig.connection as Knex.Sqlite3ConnectionConfig; + + const filename = connConfig.filename ?? ':memory:'; // If storage on disk is used, ensure that the directory exists - if ( - (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename && - (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename !== - ':memory:' - ) { - const { filename } = knexConfig.connection as Knex.Sqlite3ConnectionConfig; + if (filename !== ':memory:') { const directory = path.dirname(filename); - ensureDirSync(directory); } - const database = knexFactory(knexConfig); + let database: Knex; + + if (deps && filename === ':memory:') { + // The dev store is used during watch mode to store and restore the database + // across reloads. It is only available when running the backend through + // `backstage-cli package start`. + const devStore = DevDataStore.get(); + + if (devStore) { + const dataKey = `sqlite3-db-${deps.pluginMetadata.getId()}`; + + const connectionLoader = async () => { + // If seed data is available, use it tconnectionLoader restore the database + const { data: seedData } = await devStore.load(dataKey); + + return { + ...(knexConfig.connection as Knex.Sqlite3ConnectionConfig), + filename: seedData ?? ':memory:', + }; + }; + + database = knexFactory({ + ...knexConfig, + connection: Object.assign(connectionLoader, { + // This is a workaround for the knex SQLite driver always warning when using a config loader + filename: ':memory:', + }), + }); + + // If the dev store is available we save the database state on shutdown + deps.lifecycle.addShutdownHook(async () => { + const connection = await database.client.acquireConnection(); + const data = connection.serialize(); + await devStore.save(dataKey, data); + }); + } else { + database = knexFactory(knexConfig); + } + } else { + database = knexFactory(knexConfig); + } database.client.pool.on('createSuccess', (_eventId: any, resource: any) => { resource.run('PRAGMA foreign_keys = ON', () => {}); diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index 2d59cf636e..621bcc676b 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { Knex } from 'knex'; @@ -26,7 +30,14 @@ export interface DatabaseConnector { /** * createClient provides an instance of a knex database connector. */ - createClient(dbConfig: Config, overrides?: Partial<Knex.Config>): Knex; + createClient( + dbConfig: Config, + overrides?: Partial<Knex.Config>, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): Knex; /** * createNameOverride provides a partial knex config sufficient to override a * database name. diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index 46639a1923..afb12115b2 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -24,7 +24,6 @@ export { legacyPlugin, makeLegacyPlugin } from './legacy'; export type { LegacyCreateRouter } from './legacy'; export * from './cache'; export { loadBackendConfig } from './config'; -export * from './context'; export * from './database'; export * from './discovery'; export * from './hot'; diff --git a/packages/backend-common/src/legacy.ts b/packages/backend-common/src/legacy.ts index 30e7dd9dc5..408966f3bc 100644 --- a/packages/backend-common/src/legacy.ts +++ b/packages/backend-common/src/legacy.ts @@ -20,6 +20,7 @@ import { ServiceRef, } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; +import { cacheToPluginCacheManager } from './cache'; import { loggerToWinstonLogger } from './logging'; /** @@ -60,7 +61,7 @@ export function makeLegacyPlugin< }>, ) => { const compatPlugin = createBackendPlugin({ - id: name, + pluginId: name, register(env) { env.registerInit({ deps: { ...envMapping, _router: coreServices.httpRouter }, @@ -119,5 +120,6 @@ export const legacyPlugin = makeLegacyPlugin( }, { logger: log => loggerToWinstonLogger(log), + cache: cache => cacheToPluginCacheManager(cache), }, ); diff --git a/packages/backend-common/src/logging/loggerToWinstonLogger.ts b/packages/backend-common/src/logging/loggerToWinstonLogger.ts index f15bd5be81..4ba88358d2 100644 --- a/packages/backend-common/src/logging/loggerToWinstonLogger.ts +++ b/packages/backend-common/src/logging/loggerToWinstonLogger.ts @@ -15,6 +15,7 @@ */ import { LoggerService } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; import { Logger as WinstonLogger, createLogger } from 'winston'; import Transport, { TransportStreamOptions } from 'winston-transport'; @@ -31,7 +32,7 @@ class BackstageLoggerTransport extends Transport { callback(); return; } - const { level, message, ...meta } = info as { [name: string]: unknown }; + const { level, message, ...meta } = info as JsonObject; switch (level) { case 'error': this.backstageLogger.error(String(message), meta); diff --git a/packages/backend-common/src/scm/git.test.ts b/packages/backend-common/src/scm/git.test.ts index fdd3c7dbc8..629173f96e 100644 --- a/packages/backend-common/src/scm/git.test.ts +++ b/packages/backend-common/src/scm/git.test.ts @@ -202,7 +202,7 @@ describe('Git', () => { await git.clone({ url, dir }); const { onAuth } = ( - isomorphic.clone as unknown as jest.Mock<typeof isomorphic['clone']> + isomorphic.clone as unknown as jest.Mock<(typeof isomorphic)['clone']> ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); @@ -315,7 +315,7 @@ describe('Git', () => { await git.fetch({ remote, dir }); const { onAuth } = ( - isomorphic.fetch as unknown as jest.Mock<typeof isomorphic['fetch']> + isomorphic.fetch as unknown as jest.Mock<(typeof isomorphic)['fetch']> ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); @@ -486,7 +486,7 @@ describe('Git', () => { await git.push({ remote, dir, remoteRef, force }); const { onAuth } = ( - isomorphic.push as unknown as jest.Mock<typeof isomorphic['push']> + isomorphic.push as unknown as jest.Mock<(typeof isomorphic)['push']> ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index f74dcd9b64..f577017b77 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -102,7 +102,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { this.serverOptions.https = { certificate: { ...settings.certificate, - type: 'plain', + type: 'pem', }, }; } diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 52030d5ccf..e56a19d04a 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/backend-defaults +## 0.1.8-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-app-api@0.4.1-next.0 + +## 0.1.7 + +### Patch Changes + +- 725383f69d: Tweaked messaging in the README. +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- Updated dependencies + - @backstage/backend-app-api@0.4.0 + - @backstage/backend-plugin-api@0.4.0 + +## 0.1.7-next.2 + +### Patch Changes + +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-app-api@0.3.2-next.1 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.3.2-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.1.5 ### Patch Changes diff --git a/packages/backend-defaults/README.md b/packages/backend-defaults/README.md index 3128159f7c..2dcc5766df 100644 --- a/packages/backend-defaults/README.md +++ b/packages/backend-defaults/README.md @@ -1,8 +1,8 @@ # @backstage/backend-defaults -**This package is HIGHLY EXPERIMENTAL, do not use this for production** +**This package is EXPERIMENTAL, we recommend against using it for production deployments** -This package provides the core API used by Backstage backend apps. +This package provides the default implementations and setup for a standard Backstage backend app. ## Installation @@ -16,4 +16,5 @@ yarn add --cwd packages/backend @backstage/backend-defaults ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) +- [Backstage Backend System](https://backstage.io/docs/backend-system/) diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 5d5cc757ff..b5f6018d64 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-defaults", "description": "Backend defaults used by Backstage backend apps", - "version": "0.1.5", + "version": "0.1.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -33,6 +33,7 @@ }, "dependencies": { "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^" }, "devDependencies": { diff --git a/packages/backend-defaults/src/CreateBackend.test.ts b/packages/backend-defaults/src/CreateBackend.test.ts index 35b3a7decf..8d56cd0986 100644 --- a/packages/backend-defaults/src/CreateBackend.test.ts +++ b/packages/backend-defaults/src/CreateBackend.test.ts @@ -21,7 +21,7 @@ import { createServiceRef, createSharedEnvironment, } from '@backstage/backend-plugin-api'; -import { mockConfigFactory } from '@backstage/backend-test-utils'; +import { mockServices } from '@backstage/backend-test-utils'; import { createBackend } from './CreateBackend'; const fooServiceRef = createServiceRef<string>({ id: 'foo', scope: 'root' }); @@ -112,7 +112,7 @@ describe('createBackend', () => { }; }, }), - mockConfigFactory({ + mockServices.config.factory({ data: { root: 'root-env' }, }), createServiceFactory({ @@ -143,7 +143,7 @@ describe('createBackend', () => { expect.assertions(3); backend.add( createBackendPlugin({ - id: 'test', + pluginId: 'test', register(reg) { reg.registerInit({ deps: { diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index e6b5a15336..480f365216 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -16,22 +16,22 @@ import { Backend, - cacheFactory, - configFactory, + cacheServiceFactory, + configServiceFactory, createSpecializedBackend, - databaseFactory, - discoveryFactory, - httpRouterFactory, - rootHttpRouterFactory, - lifecycleFactory, - rootLifecycleFactory, - loggerFactory, - permissionsFactory, - rootLoggerFactory, - schedulerFactory, - tokenManagerFactory, - urlReaderFactory, - identityFactory, + databaseServiceFactory, + discoveryServiceFactory, + httpRouterServiceFactory, + rootHttpRouterServiceFactory, + lifecycleServiceFactory, + rootLifecycleServiceFactory, + loggerServiceFactory, + permissionsServiceFactory, + rootLoggerServiceFactory, + schedulerServiceFactory, + tokenManagerServiceFactory, + urlReaderServiceFactory, + identityServiceFactory, } from '@backstage/backend-app-api'; import { ServiceFactory, @@ -40,25 +40,25 @@ import { } from '@backstage/backend-plugin-api'; // Internal import of the type to avoid needing to export this. -// eslint-disable-next-line monorepo/no-internal-import +// eslint-disable-next-line @backstage/no-forbidden-package-imports import type { InternalSharedBackendEnvironment } from '@backstage/backend-plugin-api/src/wiring/createSharedEnvironment'; export const defaultServiceFactories = [ - cacheFactory(), - configFactory(), - databaseFactory(), - discoveryFactory(), - httpRouterFactory(), - identityFactory(), - lifecycleFactory(), - loggerFactory(), - permissionsFactory(), - rootHttpRouterFactory(), - rootLifecycleFactory(), - rootLoggerFactory(), - schedulerFactory(), - tokenManagerFactory(), - urlReaderFactory(), + cacheServiceFactory(), + configServiceFactory(), + databaseServiceFactory(), + discoveryServiceFactory(), + httpRouterServiceFactory(), + identityServiceFactory(), + lifecycleServiceFactory(), + loggerServiceFactory(), + permissionsServiceFactory(), + rootHttpRouterServiceFactory(), + rootLifecycleServiceFactory(), + rootLoggerServiceFactory(), + schedulerServiceFactory(), + tokenManagerServiceFactory(), + urlReaderServiceFactory(), ]; /** diff --git a/packages/backend-defaults/src/index.ts b/packages/backend-defaults/src/index.ts index b6ae2f1333..833878744e 100644 --- a/packages/backend-defaults/src/index.ts +++ b/packages/backend-defaults/src/index.ts @@ -20,5 +20,10 @@ * @packageDocumentation */ +// This import is here as a workaround for a cyclic dependency bug where +// backend-common must be loaded before backend-app-api +// TODO(Rugvip): Remove this once backend-common is no longer used by backend-app-api +import '@backstage/backend-common'; + export type { CreateBackendOptions } from './CreateBackend'; export { createBackend } from './CreateBackend'; diff --git a/packages/backend-dev-utils/.eslintrc.js b/packages/backend-dev-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/backend-dev-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend-dev-utils/CHANGELOG.md b/packages/backend-dev-utils/CHANGELOG.md new file mode 100644 index 0000000000..44238676af --- /dev/null +++ b/packages/backend-dev-utils/CHANGELOG.md @@ -0,0 +1,17 @@ +# @backstage/backend-dev-utils + +## 0.1.0 + +### Minor Changes + +- 9d9cdea117: Introduced a new package for backend development utilities. Similar to how `@backstage/dev-utils` is used in the frontend. + +### Patch Changes + +- 725383f69d: Tweaked messaging in the README. + +## 0.1.0-next.0 + +### Minor Changes + +- 9d9cdea117: Introduced a new package for backend development utilities. Similar to how `@backstage/dev-utils` is used in the frontend. diff --git a/packages/backend-dev-utils/README.md b/packages/backend-dev-utils/README.md new file mode 100644 index 0000000000..cebe591d50 --- /dev/null +++ b/packages/backend-dev-utils/README.md @@ -0,0 +1,20 @@ +# @backstage/backend-dev-utils + +**This package is EXPERIMENTAL, but we encourage use of it to add support for the new backend system in your own plugins** + +This package helps set up local development environments for Backstage backend packages. + +## Installation + +Add the library to your backend plugin or module package: + +```bash +# From your Backstage root directory +yarn add --cwd plugins/<plugin>-backend @backstage/backend-dev-utils +``` + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) +- [Backstage Backend System](https://backstage.io/docs/backend-system/) diff --git a/packages/backend-dev-utils/api-report.md b/packages/backend-dev-utils/api-report.md new file mode 100644 index 0000000000..216311ccd8 --- /dev/null +++ b/packages/backend-dev-utils/api-report.md @@ -0,0 +1,20 @@ +## API Report File for "@backstage/backend-dev-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public +export class DevDataStore { + static get(): DevDataStore | undefined; + load<T>(key: string): Promise<{ + loaded: boolean; + data: T; + }>; + save<T>( + key: string, + data: T, + ): Promise<{ + saved: boolean; + }>; +} +``` diff --git a/packages/backend-dev-utils/package.json b/packages/backend-dev-utils/package.json new file mode 100644 index 0000000000..cb1654d499 --- /dev/null +++ b/packages/backend-dev-utils/package.json @@ -0,0 +1,36 @@ +{ + "name": "@backstage/backend-dev-utils", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-dev-utils" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/packages/backend-dev-utils/src/DevDataStore.test.ts b/packages/backend-dev-utils/src/DevDataStore.test.ts new file mode 100644 index 0000000000..2bb3f221ae --- /dev/null +++ b/packages/backend-dev-utils/src/DevDataStore.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Serializable, spawn } from 'child_process'; +import { DevDataStore } from './DevDataStore'; +import { BackstageIpcClient } from './ipcClient'; + +function applyIpcTransform(value: Serializable): Promise<Serializable> { + const child = spawn( + 'node', + [ + '--eval', + ` + const listener = msg => { + process.send(msg); + process.removeListener('message', listener); + } + process.addListener('message', listener); + `, + ], + { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + serialization: 'advanced', + }, + ); + + return new Promise(resolve => { + child.addListener('message', resolve); + child.send(value); + }); +} + +class MockIpcClient implements Pick<BackstageIpcClient, 'request'> { + #data = new Map<string, Serializable>(); + + async request(method: string, body: any): Promise<any> { + if (method === 'DevDataStore.save') { + this.#data.set(body.key, body.data); + return { saved: true }; + } else if (method === 'DevDataStore.load') { + if (!this.#data.has(body.key)) { + return { loaded: false, data: undefined }; + } + const data = this.#data.get(body.key)!; + return { loaded: true, data: await applyIpcTransform(data) }; + } + throw new Error('Unknown message'); + } +} + +describe('DevDataStore', () => { + it('should save and load data', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect(store.save('test', { foo: 'bar' })).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: { foo: 'bar' }, + }); + }); + + it('should save and load buffers', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect( + store.save('test', Buffer.from('abc', 'ascii')), + ).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: Buffer.from('abc', 'ascii'), + }); + }); + + it('should save and load array buffers', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect( + store.save('test', new Uint8Array(Buffer.from('abc', 'ascii'))), + ).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: new Uint8Array(Buffer.from('abc', 'ascii')), + }); + }); + + it('should save and load buffers nested in objects', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect( + store.save('test', { + x: Buffer.from('x', 'ascii'), + y: [Buffer.from('y', 'ascii')], + }), + ).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: { x: Buffer.from('x', 'ascii'), y: [Buffer.from('y', 'ascii')] }, + }); + }); +}); diff --git a/packages/backend-dev-utils/src/DevDataStore.ts b/packages/backend-dev-utils/src/DevDataStore.ts new file mode 100644 index 0000000000..4069ab1bdb --- /dev/null +++ b/packages/backend-dev-utils/src/DevDataStore.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BackstageIpcClient, ipcClient } from './ipcClient'; + +interface SaveRequest { + key: string; + data: unknown; +} + +interface SaveResponse { + saved: boolean; +} + +interface LoadRequest { + key: string; +} + +interface LoadResponse { + loaded: boolean; + data: unknown; +} + +/** + * A data store that can be used to store temporary data during development. + * + * @public + */ +export class DevDataStore { + static #instance?: DevDataStore; + + /** + * Tries to acquire a DevDataStore instance. This will only succeed when the backend + * process is being run through the `@backstage/cli` in development mode. + * + * @returns A DevDataStore instance, or undefined if not available. + */ + static get(): DevDataStore | undefined { + if (ipcClient) { + if (!this.#instance) { + this.#instance = new DevDataStore(ipcClient); + } + return this.#instance; + } + return undefined; + } + + /** @internal */ + static forTest(client: Pick<BackstageIpcClient, 'request'>): DevDataStore { + return new DevDataStore(client as BackstageIpcClient); + } + + #client: BackstageIpcClient; + + private constructor(client: BackstageIpcClient) { + this.#client = client; + } + + /** + * Save data to the data store. + * + * @param key - The key used to identify the data. + * @param data - The data to save. The data will be serialized using advanced IPC serialization. + * @returns A promise that resolves to a result object that indicates whether the data was saved. + */ + async save<T>(key: string, data: T): Promise<{ saved: boolean }> { + return this.#client.request<SaveRequest, SaveResponse>( + 'DevDataStore.save', + { key, data }, + ); + } + + /** + * Loads data from the data store. + * + * @param key - The key used to identify the data. + * @returns A promise that resolves to a result object that indicates whether the data was loaded, as well as the data. + */ + async load<T>(key: string): Promise<{ loaded: boolean; data: T }> { + const result = await this.#client.request<LoadRequest, LoadResponse>( + 'DevDataStore.load', + { key }, + ); + return result as { loaded: boolean; data: T }; + } +} diff --git a/packages/backend-dev-utils/src/index.ts b/packages/backend-dev-utils/src/index.ts new file mode 100644 index 0000000000..9b707db029 --- /dev/null +++ b/packages/backend-dev-utils/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Local development helpers for backend. + * + * @packageDocumentation + */ + +export { DevDataStore } from './DevDataStore'; diff --git a/packages/backend-dev-utils/src/ipcClient.ts b/packages/backend-dev-utils/src/ipcClient.ts new file mode 100644 index 0000000000..87c1596a39 --- /dev/null +++ b/packages/backend-dev-utils/src/ipcClient.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type SendMessage = Exclude<typeof process.send, undefined>; + +interface Request { + id: number; + method: string; + body: unknown; + type: string; +} + +type Response = + | { + type: string; + id: number; + body: unknown; + } + | { + type: string; + id: number; + error: Error; + }; + +const requestType = '@backstage/cli/channel/request'; +const responseType = '@backstage/cli/channel/response'; + +/** + * The client side of an IPC communication channel. + * + * @internal + */ +export class BackstageIpcClient { + #messageId = 0; + #sendMessage: SendMessage; + + /** + * Creates a new client if we're in a child process with IPC and BACKSTAGE_CLI_CHANNEL is set. + */ + static create(): BackstageIpcClient | undefined { + const sendMessage = process.send?.bind(process); + return sendMessage && process.env.BACKSTAGE_CLI_CHANNEL + ? new BackstageIpcClient(sendMessage) + : undefined; + } + + constructor(sendMessage: SendMessage) { + this.#sendMessage = sendMessage; + } + + /** + * Send a request to the parent process and wait for a response. + */ + async request<TRequestBody, TResponseBody>( + method: string, + body: TRequestBody, + ): Promise<TResponseBody> { + return new Promise((resolve, reject) => { + const id = this.#messageId++; + + const request: Request = { + type: requestType, + id, + method, + body, + }; + + this.#sendMessage(request, (e: Error) => { + if (e) { + reject(e); + return; + } + + let timeout: NodeJS.Timeout | undefined = undefined; + + const messageHandler = (response: Response) => { + if (response?.type !== responseType) { + return; + } + if (response.id !== id) { + return; + } + + if ('error' in response) { + const error = new Error(response.error.message); + if (response.error.name) { + error.name = response.error.name; + } + reject(error); + } else { + resolve(response.body as TResponseBody); + } + + clearTimeout(timeout); + process.removeListener('message', messageHandler); + }; + + timeout = setTimeout(() => { + reject(new Error(`IPC request '${method}' with ID ${id} timed out`)); + process.removeListener('message', messageHandler); + }, 5000); + timeout.unref(); + + process.addListener('message', messageHandler as () => void); + }); + }); + } +} + +export const ipcClient = BackstageIpcClient.create(); diff --git a/packages/backend-dev-utils/src/setupTests.ts b/packages/backend-dev-utils/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/packages/backend-dev-utils/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md index 883fae9f38..2c40502783 100644 --- a/packages/backend-next/CHANGELOG.md +++ b/packages/backend-next/CHANGELOG.md @@ -1,5 +1,56 @@ # example-backend-next +## 0.0.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-todo-backend@0.1.40-next.0 + - @backstage/plugin-scaffolder-backend@1.11.1-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-defaults@0.1.8-next.0 + - @backstage/plugin-app-backend@0.3.43-next.0 + +## 0.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/plugin-scaffolder-backend@1.11.0 + - @backstage/plugin-app-backend@0.3.42 + - @backstage/backend-defaults@0.1.7 + +## 0.0.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-app-backend@0.3.42-next.2 + - @backstage/backend-defaults@0.1.7-next.2 + +## 0.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/plugin-scaffolder-backend@1.11.0-next.1 + - @backstage/backend-defaults@0.1.7-next.1 + - @backstage/plugin-app-backend@0.3.42-next.1 + +## 0.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.11.0-next.0 + - @backstage/backend-defaults@0.1.7-next.0 + - @backstage/plugin-app-backend@0.3.42-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + ## 0.0.7 ### Patch Changes diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index 19588986ae..21ba3550b4 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-next", - "version": "0.0.7", + "version": "0.0.9-next.0", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,7 +18,7 @@ "backstage" ], "scripts": { - "start": "backstage-cli package start", + "start": "EXPERIMENTAL_BACKEND_START=1 backstage-cli package start", "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", @@ -28,7 +28,8 @@ "@backstage/backend-defaults": "workspace:^", "@backstage/plugin-app-backend": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", - "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-todo-backend": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index 8196b28452..8e29244ebf 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -14,14 +14,16 @@ * limitations under the License. */ -import { catalogPlugin } from '@backstage/plugin-catalog-backend'; -import { scaffolderCatalogModule } from '@backstage/plugin-scaffolder-backend'; +import { catalogPlugin } from '@backstage/plugin-catalog-backend/alpha'; +import { catalogModuleTemplateKind } from '@backstage/plugin-scaffolder-backend/alpha'; import { createBackend } from '@backstage/backend-defaults'; -import { appPlugin } from '@backstage/plugin-app-backend'; +import { appPlugin } from '@backstage/plugin-app-backend/alpha'; +import { todoPlugin } from '@backstage/plugin-todo-backend'; const backend = createBackend(); backend.add(catalogPlugin()); -backend.add(scaffolderCatalogModule()); +backend.add(catalogModuleTemplateKind()); backend.add(appPlugin({ appPackageName: 'example-app' })); +backend.add(todoPlugin()); backend.start(); diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index 6bafe56b2a..4dc19db6fc 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,91 @@ # @backstage/backend-plugin-api +## 0.4.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## 0.4.0 + +### Minor Changes + +- e716946103: **BREAKING**: Split out the hook for both lifecycle services so that the first parameter of `addShutdownHook` is the hook function, and the second is the options. +- 0ff03319be: **BREAKING**: The plugin ID option passed to `createBackendPlugin` is now `pluginId`, rather than just `id`. This is to make it match `createBackendModule` more closely. +- 71a5ec0f06: **BREAKING**: Switched out `LogMeta` type for `JsonObject`. +- 5febb216fe: **BREAKING**: The `CacheService` has been changed to remove the indirection of `getClient`, instead making the `CacheClient` methods directly available on the `CacheService`. In order to allow for the creation of clients with default options, there is now a new `.withOptions` method that must be implemented as part of the service interface. +- b86efa2d04: Switch `ServiceFactory` to be an opaque type, keeping only the `service` field as public API, but also adding a type parameter for the service scope. +- 610d65e143: Switched `BackendFeature` to be an opaque type. + +### Patch Changes + +- 9c9456fd33: Removed the unused `TypesToServiceRef` type +- 181c03edb5: Aligned opaque type markers to all use a `$type` property with namespacing. +- 725383f69d: Tweaked messaging in the README. +- ae88f61e00: The `register` methods passed to `createBackendPlugin` and `createBackendModule` + now have dedicated `BackendPluginRegistrationPoints` and + `BackendModuleRegistrationPoints` arguments, respectively. This lets us make it + clear on a type level that it's not possible to pass in extension points as + dependencies to plugins (should only ever be done for modules). This has no + practical effect on code that was already well behaved. +- Updated dependencies + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + +## 0.4.0-next.2 + +### Minor Changes + +- e716946103: **BREAKING**: Split out the hook for both lifecycle services so that the first parameter of `addShutdownHook` is the hook function, and the second is the options. +- 0ff03319be: **BREAKING**: The plugin ID option passed to `createBackendPlugin` is now `pluginId`, rather than just `id`. This is to make it match `createBackendModule` more closely. +- 71a5ec0f06: **BREAKING**: Switched out `LogMeta` type for `JsonObject`. +- 610d65e143: Switched `BackendFeature` to be an opaque type. + +### Patch Changes + +- 9c9456fd33: Removed the unused `TypesToServiceRef` type +- 181c03edb5: Aligned opaque type markers to all use a `$type` property with namespacing. +- Updated dependencies + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + +## 0.3.2-next.1 + +### Patch Changes + +- ae88f61e00: The `register` methods passed to `createBackendPlugin` and `createBackendModule` + now have dedicated `BackendPluginRegistrationPoints` and + `BackendModuleRegistrationPoints` arguments, respectively. This lets us make it + clear on a type level that it's not possible to pass in extension points as + dependencies to plugins (should only ever be done for modules). This has no + practical effect on code that was already well behaved. +- Updated dependencies + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## 0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + ## 0.3.0 ### Minor Changes diff --git a/packages/backend-plugin-api/README.md b/packages/backend-plugin-api/README.md index 364655dd60..a17c8715a7 100644 --- a/packages/backend-plugin-api/README.md +++ b/packages/backend-plugin-api/README.md @@ -1,19 +1,20 @@ # @backstage/backend-plugin-api -**This package is HIGHLY EXPERIMENTAL, do not use this for production** +**This package is EXPERIMENTAL, but we encourage use of it to add support for the new backend system in your own plugins** -This package provides the core API used by Backstage backend plugins. +This package provides the core API used by Backstage backend plugins and modules. ## Installation -Add the library to your backend plugin package: +Add the library to your backend plugin or module package: ```bash # From your Backstage root directory -yarn add --cwd plugin/<plugin>-backend @backstage/backend-plugin-api +yarn add --cwd plugins/<plugin>-backend @backstage/backend-plugin-api ``` ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) +- [Backstage Backend System](https://backstage.io/docs/backend-system/) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 86917dae11..5e64502b78 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -8,6 +8,7 @@ import { Config } from '@backstage/config'; import { Handler } from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; +import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -17,38 +18,19 @@ import { Readable } from 'stream'; // @public (undocumented) export interface BackendFeature { // (undocumented) - id: string; - // (undocumented) - register(reg: BackendRegistrationPoints): void; + $$type: '@backstage/BackendFeature'; } -// @public (undocumented) +// @public export interface BackendModuleConfig { - // (undocumented) moduleId: string; - // (undocumented) pluginId: string; // (undocumented) - register( - reg: Omit<BackendRegistrationPoints, 'registerExtensionPoint'>, - ): void; + register(reg: BackendModuleRegistrationPoints): void; } -// @public (undocumented) -export interface BackendPluginConfig { - // (undocumented) - id: string; - // (undocumented) - register(reg: BackendRegistrationPoints): void; -} - -// @public (undocumented) -export interface BackendRegistrationPoints { - // (undocumented) - registerExtensionPoint<TExtensionPoint>( - ref: ExtensionPoint<TExtensionPoint>, - impl: TExtensionPoint, - ): void; +// @public +export interface BackendModuleRegistrationPoints { // (undocumented) registerInit< Deps extends { @@ -63,31 +45,54 @@ export interface BackendRegistrationPoints { } // @public -export interface CacheClient { - delete(key: string): Promise<void>; - get(key: string): Promise<JsonValue | undefined>; - set( - key: string, - value: JsonValue, - options?: CacheClientSetOptions, - ): Promise<void>; +export interface BackendPluginConfig { + pluginId: string; + // (undocumented) + register(reg: BackendPluginRegistrationPoints): void; } // @public -export type CacheClientOptions = { +export interface BackendPluginRegistrationPoints { + // (undocumented) + registerExtensionPoint<TExtensionPoint>( + ref: ExtensionPoint<TExtensionPoint>, + impl: TExtensionPoint, + ): void; + // (undocumented) + registerInit< + Deps extends { + [name in string]: unknown; + }, + >(options: { + deps: { + [name in keyof Deps]: ServiceRef<Deps[name]>; + }; + init(deps: Deps): Promise<void>; + }): void; +} + +// @public +export interface CacheService { + delete(key: string): Promise<void>; + get<TValue extends JsonValue>(key: string): Promise<TValue | undefined>; + set( + key: string, + value: JsonValue, + options?: CacheServiceSetOptions, + ): Promise<void>; + withOptions(options: CacheServiceOptions): CacheService; +} + +// @public +export type CacheServiceOptions = { defaultTtl?: number; }; // @public -export type CacheClientSetOptions = { +export type CacheServiceSetOptions = { ttl?: number; }; -// @public -export interface CacheService { - getClient: (options?: CacheClientOptions) => CacheClient; -} - // @public (undocumented) export interface ConfigService extends Config {} @@ -116,12 +121,12 @@ export function createBackendModule<TOptions extends [options?: object] = []>( config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig), ): (...params: TOptions) => BackendFeature; -// @public (undocumented) +// @public export function createBackendPlugin<TOptions extends [options?: object] = []>( config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig), ): (...params: TOptions) => BackendFeature; -// @public (undocumented) +// @public export function createExtensionPoint<T>( config: ExtensionPointConfig, ): ExtensionPoint<T>; @@ -136,7 +141,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: RootServiceFactoryConfig<TService, TImpl, TDeps>, -): () => ServiceFactory<TService>; +): () => ServiceFactory<TService, 'root'>; // @public export function createServiceFactory< @@ -148,7 +153,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: (options?: TOpts) => RootServiceFactoryConfig<TService, TImpl, TDeps>, -): (options?: TOpts) => ServiceFactory<TService>; +): (options?: TOpts) => ServiceFactory<TService, 'root'>; // @public export function createServiceFactory< @@ -160,7 +165,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: (options: TOpts) => RootServiceFactoryConfig<TService, TImpl, TDeps>, -): (options: TOpts) => ServiceFactory<TService>; +): (options: TOpts) => ServiceFactory<TService, 'root'>; // @public export function createServiceFactory< @@ -173,7 +178,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>, -): () => ServiceFactory<TService>; +): () => ServiceFactory<TService, 'plugin'>; // @public export function createServiceFactory< @@ -188,7 +193,7 @@ export function createServiceFactory< config: ( options?: TOpts, ) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>, -): (options?: TOpts) => ServiceFactory<TService>; +): (options?: TOpts) => ServiceFactory<TService, 'plugin'>; // @public export function createServiceFactory< @@ -205,7 +210,7 @@ export function createServiceFactory< | (( options: TOpts, ) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>), -): (options: TOpts) => ServiceFactory<TService>; +): (options: TOpts) => ServiceFactory<TService, 'plugin'>; // @public export function createServiceRef<TService>( @@ -245,12 +250,11 @@ export type ExtensionPoint<T> = { id: string; T: T; toString(): string; - $$ref: 'extension-point'; + $$type: '@backstage/ExtensionPoint'; }; -// @public (undocumented) +// @public export interface ExtensionPointConfig { - // (undocumented) id: string; } @@ -265,34 +269,34 @@ export interface IdentityService extends IdentityApi {} // @public (undocumented) export interface LifecycleService { - addShutdownHook(options: LifecycleServiceShutdownHook): void; + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void; } // @public (undocumented) -export type LifecycleServiceShutdownHook = { - fn: () => void | Promise<void>; +export type LifecycleServiceShutdownHook = () => void | Promise<void>; + +// @public (undocumented) +export interface LifecycleServiceShutdownOptions { logger?: LoggerService; -}; +} // @public export interface LoggerService { // (undocumented) - child(meta: LogMeta): LoggerService; + child(meta: JsonObject): LoggerService; // (undocumented) - debug(message: string, meta?: Error | LogMeta): void; + debug(message: string, meta?: Error | JsonObject): void; // (undocumented) - error(message: string, meta?: Error | LogMeta): void; + error(message: string, meta?: Error | JsonObject): void; // (undocumented) - info(message: string, meta?: Error | LogMeta): void; + info(message: string, meta?: Error | JsonObject): void; // (undocumented) - warn(message: string, meta?: Error | LogMeta): void; + warn(message: string, meta?: Error | JsonObject): void; } -// @public (undocumented) -export type LogMeta = { - [name: string]: unknown; -}; - // @public (undocumented) export interface PermissionsService extends PermissionEvaluator {} @@ -419,38 +423,18 @@ export type SearchResponseFile = { }; // @public (undocumented) -export type ServiceFactory<TService = unknown> = - | { - scope: 'root'; - service: ServiceRef<TService, 'root'>; - deps: { - [key in string]: ServiceRef<unknown>; - }; - factory(deps: { - [key in string]: unknown; - }): Promise<TService>; - } - | { - scope: 'plugin'; - service: ServiceRef<TService, 'plugin'>; - deps: { - [key in string]: ServiceRef<unknown>; - }; - createRootContext?(deps: { - [key in string]: unknown; - }): Promise<unknown>; - factory( - deps: { - [key in string]: unknown; - }, - context: unknown, - ): Promise<TService>; - }; +export interface ServiceFactory< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', +> { + // (undocumented) + $$type: '@backstage/ServiceFactory'; + // (undocumented) + service: ServiceRef<TService, TScope>; +} // @public -export type ServiceFactoryOrFunction<TService = unknown> = - | ServiceFactory<TService> - | (() => ServiceFactory<TService>); +export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); // @public export type ServiceRef< @@ -461,7 +445,7 @@ export type ServiceRef< scope: TScope; T: TService; toString(): string; - $$ref: 'service'; + $$type: '@backstage/ServiceRef'; }; // @public (undocumented) @@ -469,20 +453,20 @@ export interface ServiceRefConfig<TService, TScope extends 'root' | 'plugin'> { // (undocumented) defaultFactory?: ( service: ServiceRef<TService, TScope>, - ) => Promise<ServiceFactoryOrFunction<TService>>; + ) => Promise<ServiceFactoryOrFunction>; // (undocumented) id: string; // (undocumented) scope?: TScope; } -// @public (undocumented) +// @public export interface SharedBackendEnvironment { // (undocumented) - $$type: 'SharedBackendEnvironment'; + $$type: '@backstage/SharedBackendEnvironment'; } -// @public (undocumented) +// @public export interface SharedBackendEnvironmentConfig { // (undocumented) services?: ServiceFactoryOrFunction[]; @@ -496,11 +480,6 @@ export interface TokenManagerService { }>; } -// @public (undocumented) -export type TypesToServiceRef<T> = { - [key in keyof T]: ServiceRef<T[key]>; -}; - // @public export interface UrlReaderService { readTree(url: string, options?: ReadTreeOptions): Promise<ReadTreeResponse>; diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 9008b910c2..06acbce628 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,14 +1,13 @@ { "name": "@backstage/backend-plugin-api", "description": "Core API used by Backstage backend plugins", - "version": "0.3.0", + "version": "0.4.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "types": "dist/index.d.ts" }, "backstage": { "role": "node-library" @@ -24,7 +23,7 @@ ], "license": "Apache-2.0", "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", diff --git a/packages/backend-plugin-api/src/services/definitions/CacheService.ts b/packages/backend-plugin-api/src/services/definitions/CacheService.ts index 2702fdac6d..ccf88823b0 100644 --- a/packages/backend-plugin-api/src/services/definitions/CacheService.ts +++ b/packages/backend-plugin-api/src/services/definitions/CacheService.ts @@ -17,29 +17,11 @@ import { JsonValue } from '@backstage/types'; /** - * Manages access to cache stores that plugins get. + * Options passed to {@link CacheService.set}. * * @public */ -export interface CacheService { - /** - * Provides backend plugins cache connections for themselves. - * - * @remarks - * - * The purpose of this method is to allow plugins to get isolated data stores - * so that plugins are discouraged from cache-level integration and/or cache - * key collisions. - */ - getClient: (options?: CacheClientOptions) => CacheClient; -} - -/** - * Options passed to {@link CacheClient.set}. - * - * @public - */ -export type CacheClientSetOptions = { +export type CacheServiceSetOptions = { /** * Optional TTL in milliseconds. Defaults to the TTL provided when the client * was set up (or no TTL if none are provided). @@ -48,17 +30,31 @@ export type CacheClientSetOptions = { }; /** - * A pre-configured, storage agnostic cache client suitable for use by + * Options passed to {@link CacheService.withOptions}. + * + * @public + */ +export type CacheServiceOptions = { + /** + * An optional default TTL (in milliseconds) to be set when getting a client + * instance. If not provided, data will persist indefinitely by default (or + * can be configured per entry at set-time). + */ + defaultTtl?: number; +}; + +/** + * A pre-configured, storage agnostic cache service suitable for use by * Backstage plugins. * * @public */ -export interface CacheClient { +export interface CacheService { /** * Reads data from a cache store for the given key. If no data was found, * returns undefined. */ - get(key: string): Promise<JsonValue | undefined>; + get<TValue extends JsonValue>(key: string): Promise<TValue | undefined>; /** * Writes the given data to a cache store, associated with the given key. An @@ -68,25 +64,16 @@ export interface CacheClient { set( key: string, value: JsonValue, - options?: CacheClientSetOptions, + options?: CacheServiceSetOptions, ): Promise<void>; /** * Removes the given key from the cache store. */ delete(key: string): Promise<void>; -} -/** - * Options given when constructing a {@link CacheClient}. - * - * @public - */ -export type CacheClientOptions = { /** - * An optional default TTL (in milliseconds) to be set when getting a client - * instance. If not provided, data will persist indefinitely by default (or - * can be configured per entry at set-time). + * Creates a new {@link CacheService} instance with the given options. */ - defaultTtl?: number; -}; + withOptions(options: CacheServiceOptions): CacheService; +} diff --git a/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts b/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts index 25cc47270e..89351194e6 100644 --- a/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts +++ b/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts @@ -18,22 +18,28 @@ import { LoggerService } from './LoggerService'; /** * @public - **/ -export type LifecycleServiceShutdownHook = { - fn: () => void | Promise<void>; + */ +export type LifecycleServiceShutdownHook = () => void | Promise<void>; +/** + * @public + */ +export interface LifecycleServiceShutdownOptions { /** * Optional {@link LoggerService} that will be used for logging instead of the default logger. */ logger?: LoggerService; -}; +} /** * @public - **/ + */ export interface LifecycleService { /** * Register a function to be called when the backend is shutting down. */ - addShutdownHook(options: LifecycleServiceShutdownHook): void; + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void; } diff --git a/packages/backend-plugin-api/src/services/definitions/LoggerService.ts b/packages/backend-plugin-api/src/services/definitions/LoggerService.ts index 269dfa0166..2e4edc60be 100644 --- a/packages/backend-plugin-api/src/services/definitions/LoggerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/LoggerService.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -/** - * @public - */ -export type LogMeta = { [name: string]: unknown }; +import { JsonObject } from '@backstage/types'; /** * A service that provides a logging facility. @@ -25,10 +22,10 @@ export type LogMeta = { [name: string]: unknown }; * @public */ export interface LoggerService { - error(message: string, meta?: Error | LogMeta): void; - warn(message: string, meta?: Error | LogMeta): void; - info(message: string, meta?: Error | LogMeta): void; - debug(message: string, meta?: Error | LogMeta): void; + error(message: string, meta?: Error | JsonObject): void; + warn(message: string, meta?: Error | JsonObject): void; + info(message: string, meta?: Error | JsonObject): void; + debug(message: string, meta?: Error | JsonObject): void; - child(meta: LogMeta): LoggerService; + child(meta: JsonObject): LoggerService; } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index c5af0967bd..0122bc0be8 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -17,9 +17,8 @@ export { coreServices } from './coreServices'; export type { CacheService, - CacheClient, - CacheClientOptions, - CacheClientSetOptions, + CacheServiceOptions, + CacheServiceSetOptions, } from './CacheService'; export type { ConfigService } from './ConfigService'; export type { DatabaseService } from './DatabaseService'; @@ -28,8 +27,9 @@ export type { HttpRouterService } from './HttpRouterService'; export type { LifecycleService, LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, } from './LifecycleService'; -export type { LoggerService, LogMeta } from './LoggerService'; +export type { LoggerService } from './LoggerService'; export type { PermissionsService } from './PermissionsService'; export type { PluginMetadataService } from './PluginMetadataService'; export type { RootHttpRouterService } from './RootHttpRouterService'; diff --git a/packages/backend-plugin-api/src/services/system/index.ts b/packages/backend-plugin-api/src/services/system/index.ts index 74be9f8b11..2b91758cbc 100644 --- a/packages/backend-plugin-api/src/services/system/index.ts +++ b/packages/backend-plugin-api/src/services/system/index.ts @@ -17,7 +17,6 @@ export type { ServiceRef, ServiceRefConfig, - TypesToServiceRef, ServiceFactory, PluginServiceFactoryConfig, RootServiceFactoryConfig, diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 2615c5e3a3..190ef7c13e 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -44,41 +44,39 @@ export type ServiceRef< toString(): string; - $$ref: 'service'; + $$type: '@backstage/ServiceRef'; }; /** @public */ -export type TypesToServiceRef<T> = { [key in keyof T]: ServiceRef<T[key]> }; +export interface ServiceFactory< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', +> { + $$type: '@backstage/ServiceFactory'; -/** @public */ -export type ServiceFactory<TService = unknown> = - | { - // This scope prop is needed in addition to the service ref, as TypeScript - // can't properly discriminate the two factory types otherwise. - scope: 'root'; - service: ServiceRef<TService, 'root'>; - deps: { [key in string]: ServiceRef<unknown> }; - factory(deps: { [key in string]: unknown }): Promise<TService>; - } - | { - scope: 'plugin'; - service: ServiceRef<TService, 'plugin'>; - deps: { [key in string]: ServiceRef<unknown> }; - createRootContext?(deps: { [key in string]: unknown }): Promise<unknown>; - factory( - deps: { [key in string]: unknown }, - context: unknown, - ): Promise<TService>; - }; + service: ServiceRef<TService, TScope>; +} + +/** @internal */ +export interface InternalServiceFactory< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', +> extends ServiceFactory<TService, TScope> { + version: 'v1'; + deps: { [key in string]: ServiceRef<unknown> }; + createRootContext?(deps: { [key in string]: unknown }): Promise<unknown>; + factory( + deps: { [key in string]: unknown }, + context: unknown, + ): Promise<TService>; +} /** * Represents either a {@link ServiceFactory} or a function that returns one. * * @public */ -export type ServiceFactoryOrFunction<TService = unknown> = - | ServiceFactory<TService> - | (() => ServiceFactory<TService>); +export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); /** @public */ export interface ServiceRefConfig<TService, TScope extends 'root' | 'plugin'> { @@ -86,7 +84,7 @@ export interface ServiceRefConfig<TService, TScope extends 'root' | 'plugin'> { scope?: TScope; defaultFactory?: ( service: ServiceRef<TService, TScope>, - ) => Promise<ServiceFactoryOrFunction<TService>>; + ) => Promise<ServiceFactoryOrFunction>; } /** @@ -119,7 +117,7 @@ export function createServiceRef<TService>( toString() { return `serviceRef{${config.id}}`; }, - $$ref: 'service', // TODO: declare + $$type: '@backstage/ServiceRef', __defaultFactory: defaultFactory, } as ServiceRef<TService, typeof scope> & { __defaultFactory?: ( @@ -178,7 +176,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: RootServiceFactoryConfig<TService, TImpl, TDeps>, -): () => ServiceFactory<TService>; +): () => ServiceFactory<TService, 'root'>; /** * Creates a root scoped service factory with optional options. * @@ -192,7 +190,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: (options?: TOpts) => RootServiceFactoryConfig<TService, TImpl, TDeps>, -): (options?: TOpts) => ServiceFactory<TService>; +): (options?: TOpts) => ServiceFactory<TService, 'root'>; /** * Creates a root scoped service factory with required options. * @@ -206,7 +204,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: (options: TOpts) => RootServiceFactoryConfig<TService, TImpl, TDeps>, -): (options: TOpts) => ServiceFactory<TService>; +): (options: TOpts) => ServiceFactory<TService, 'root'>; /** * Creates a plugin scoped service factory without options. * @@ -221,7 +219,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>, -): () => ServiceFactory<TService>; +): () => ServiceFactory<TService, 'plugin'>; /** * Creates a plugin scoped service factory with optional options. * @@ -238,7 +236,7 @@ export function createServiceFactory< config: ( options?: TOpts, ) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>, -): (options?: TOpts) => ServiceFactory<TService>; +): (options?: TOpts) => ServiceFactory<TService, 'plugin'>; /** * Creates a plugin scoped service factory with required options. * @@ -257,7 +255,7 @@ export function createServiceFactory< | (( options: TOpts, ) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>), -): (options: TOpts) => ServiceFactory<TService>; +): (options: TOpts) => ServiceFactory<TService, 'plugin'>; export function createServiceFactory< TService, TImpl extends TService, @@ -274,20 +272,40 @@ export function createServiceFactory< ) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>) | (() => RootServiceFactoryConfig<TService, TImpl, TDeps>) | (() => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>), -): (options: TOpts) => ServiceFactory<TService> { +): (options: TOpts) => ServiceFactory { const configCallback = typeof config === 'function' ? config : () => config; - return (options: TOpts) => { - const c = configCallback(options); + return ( + options: TOpts, + ): InternalServiceFactory<TService, 'plugin' | 'root'> => { + const anyConf = configCallback(options); + if (anyConf.service.scope === 'root') { + const c = anyConf as RootServiceFactoryConfig<TService, TImpl, TDeps>; + return { + $$type: '@backstage/ServiceFactory', + version: 'v1', + service: c.service, + deps: c.deps, + factory: async (deps: TDeps) => c.factory(deps), + }; + } + const c = anyConf as PluginServiceFactoryConfig< + TService, + TContext, + TImpl, + TDeps + >; return { - ...c, + $$type: '@backstage/ServiceFactory', + version: 'v1', + service: c.service, ...('createRootContext' in c ? { createRootContext: async (deps: TDeps) => c?.createRootContext?.(deps), } : {}), + deps: c.deps, factory: async (deps: TDeps, ctx: TContext) => c.factory(deps, ctx), - scope: c.service.scope, - } as ServiceFactory<TService>; + }; }; } diff --git a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts index ec2cf1fe49..381a4799d2 100644 --- a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts +++ b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts @@ -48,7 +48,7 @@ describe('createSharedEnvironment', () => { expect(env).toBeDefined(); const internalEnv = env() as unknown as InternalSharedBackendEnvironment; expect(internalEnv).toEqual({ - $$type: 'SharedBackendEnvironment', + $$type: '@backstage/SharedBackendEnvironment', version: 'v1', services: undefined, }); diff --git a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts index 3258188772..57f0533523 100644 --- a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts +++ b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts @@ -16,35 +16,47 @@ import { ServiceFactory, ServiceFactoryOrFunction } from '../services'; -/** @public */ +/** + * The configuration options passed to {@link createSharedEnvironment}. + * + * @public + */ export interface SharedBackendEnvironmentConfig { services?: ServiceFactoryOrFunction[]; } -// This type is opaque in order to allow for future API evolution without -// cluttering the external API. For example we might want to add support -// for more powerful callback based backend modifications. -// -// By making this opaque we also ensure that the type doesn't become an input -// type that we need to care about, as it would otherwise be possible to pass -// a custom environment definition to `createBackend`, which we don't want. /** + * An opaque type that represents the contents of a shared backend environment. + * * @public */ export interface SharedBackendEnvironment { - $$type: 'SharedBackendEnvironment'; + $$type: '@backstage/SharedBackendEnvironment'; + + // NOTE: This type is opaque in order to allow for future API evolution without + // cluttering the external API. For example we might want to add support + // for more powerful callback based backend modifications. + // + // By making this opaque we also ensure that the type doesn't become an input + // type that we need to care about, as it would otherwise be possible to pass + // a custom environment definition to `createBackend`, which we don't want. } /** - * This type is NOT supposed to be used by anyone except internally by the backend-app-api package. - * @internal */ + * This type is NOT supposed to be used by anyone except internally by the + * backend-app-api package. + * + * @internal + */ export interface InternalSharedBackendEnvironment { version: 'v1'; services?: ServiceFactory[]; } /** - * Creates a shared backend environment which can be used to create multiple backends + * Creates a shared backend environment which can be used to create multiple + * backends. + * * @public */ export function createSharedEnvironment< @@ -81,7 +93,7 @@ export function createSharedEnvironment< // Here to ensure type safety in this internal implementation. const env: SharedBackendEnvironment & InternalSharedBackendEnvironment = { - $$type: 'SharedBackendEnvironment', + $$type: '@backstage/SharedBackendEnvironment', version: 'v1', services, }; diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/factories.test.ts index dbdf0ead14..ae199d84e4 100644 --- a/packages/backend-plugin-api/src/wiring/factories.test.ts +++ b/packages/backend-plugin-api/src/wiring/factories.test.ts @@ -19,6 +19,7 @@ import { createBackendPlugin, createExtensionPoint, } from './factories'; +import { InternalBackendFeature } from './types'; describe('createExtensionPoint', () => { it('should create an ExtensionPoint', () => { @@ -33,12 +34,31 @@ describe('createExtensionPoint', () => { describe('createBackendPlugin', () => { it('should create a BackendPlugin', () => { const plugin = createBackendPlugin((_options: { a: string }) => ({ - id: 'x', - register() {}, + pluginId: 'x', + register(r) { + r.registerInit({ deps: {}, async init() {} }); + }, })); expect(plugin).toBeDefined(); expect(plugin({ a: 'a' })).toBeDefined(); - expect(plugin({ a: 'a' }).id).toBe('x'); + expect(plugin({ a: 'a' })).toEqual({ + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations: expect.any(Function), + }); + expect( + (plugin({ a: 'a' }) as InternalBackendFeature).getRegistrations(), + ).toEqual([ + { + type: 'plugin', + pluginId: 'x', + extensionPoints: [], + init: { + deps: expect.any(Object), + func: expect.any(Function), + }, + }, + ]); // @ts-expect-error expect(plugin()).toBeDefined(); // @ts-expect-error @@ -47,7 +67,7 @@ describe('createBackendPlugin', () => { it('should create plugins with optional options', () => { const plugin = createBackendPlugin((_options?: { a: string }) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); @@ -59,7 +79,7 @@ describe('createBackendPlugin', () => { it('should create plugins without options', () => { const plugin = createBackendPlugin({ - id: 'x', + pluginId: 'x', register() {}, }); expect(plugin).toBeDefined(); @@ -74,12 +94,16 @@ describe('createBackendPlugin', () => { a: string; } const plugin = createBackendPlugin((_options: TestOptions) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); expect(plugin({ a: 'a' })).toBeDefined(); - expect(plugin({ a: 'a' }).id).toBe('x'); + expect(plugin({ a: 'a' })).toEqual({ + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations: expect.any(Function), + }); // @ts-expect-error expect(plugin()).toBeDefined(); // @ts-expect-error @@ -91,7 +115,7 @@ describe('createBackendPlugin', () => { a: string; } const plugin = createBackendPlugin((_options?: TestOptions) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); @@ -107,11 +131,30 @@ describe('createBackendModule', () => { const mod = createBackendModule((_options: { a: string }) => ({ pluginId: 'x', moduleId: 'y', - register() {}, + register(r) { + r.registerInit({ deps: {}, async init() {} }); + }, })); expect(mod).toBeDefined(); expect(mod({ a: 'a' })).toBeDefined(); - expect(mod({ a: 'a' }).id).toBe('x.y'); + expect(mod({ a: 'a' })).toEqual({ + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations: expect.any(Function), + }); + expect( + (mod({ a: 'a' }) as InternalBackendFeature).getRegistrations(), + ).toEqual([ + { + type: 'module', + pluginId: 'x', + moduleId: 'y', + init: { + deps: expect.any(Object), + func: expect.any(Function), + }, + }, + ]); // @ts-expect-error expect(mod()).toBeDefined(); // @ts-expect-error @@ -155,7 +198,11 @@ describe('createBackendModule', () => { })); expect(mod).toBeDefined(); expect(mod({ a: 'a' })).toBeDefined(); - expect(mod({ a: 'a' }).id).toBe('x.y'); + expect(mod({ a: 'a' })).toEqual({ + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations: expect.any(Function), + }); // @ts-expect-error expect(mod()).toBeDefined(); // @ts-expect-error diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts index 31a64778f2..928c933af0 100644 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ b/packages/backend-plugin-api/src/wiring/factories.ts @@ -15,17 +15,37 @@ */ import { - BackendRegistrationPoints, + BackendModuleRegistrationPoints, + BackendPluginRegistrationPoints, BackendFeature, ExtensionPoint, + InternalBackendFeature, + InternalBackendModuleRegistration, + InternalBackendPluginRegistration, } from './types'; -/** @public */ +/** + * The configuration options passed to {@link createExtensionPoint}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ export interface ExtensionPointConfig { + /** + * The ID of this extension point. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ id: string; } -/** @public */ +/** + * Creates a new backend extension point. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} + */ export function createExtensionPoint<T>( config: ExtensionPointConfig, ): ExtensionPoint<T> { @@ -37,67 +57,170 @@ export function createExtensionPoint<T>( toString() { return `extensionPoint{${config.id}}`; }, - $$ref: 'extension-point', // TODO: declare + $$type: '@backstage/ExtensionPoint', }; } -/** @public */ +/** + * The configuration options passed to {@link createBackendPlugin}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ export interface BackendPluginConfig { - id: string; - register(reg: BackendRegistrationPoints): void; + /** + * The ID of this plugin. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + pluginId: string; + register(reg: BackendPluginRegistrationPoints): void; } -/** @public */ +/** + * Creates a new backend plugin. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ export function createBackendPlugin<TOptions extends [options?: object] = []>( config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig), ): (...params: TOptions) => BackendFeature { - if (typeof config === 'function') { - return config; - } + const configCallback = typeof config === 'function' ? config : () => config; + return (...options: TOptions): InternalBackendFeature => { + const c = configCallback(...options); - return () => config; + let registrations: InternalBackendPluginRegistration[]; + + return { + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations() { + if (registrations) { + return registrations; + } + const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = + []; + let init: InternalBackendPluginRegistration['init'] | undefined = + undefined; + + c.register({ + registerExtensionPoint(ext, impl) { + if (init) { + throw new Error( + 'registerExtensionPoint called after registerInit', + ); + } + extensionPoints.push([ext, impl]); + }, + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${c.pluginId}`, + ); + } + + registrations = [ + { + type: 'plugin', + pluginId: c.pluginId, + extensionPoints, + init, + }, + ]; + return registrations; + }, + }; + }; } -/** @public */ +/** + * The configuration options passed to {@link createBackendModule}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ export interface BackendModuleConfig { + /** + * The ID of this plugin. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ pluginId: string; + /** + * Should exactly match the `id` of the plugin that the module extends. + */ moduleId: string; - register( - reg: Omit<BackendRegistrationPoints, 'registerExtensionPoint'>, - ): void; + register(reg: BackendModuleRegistrationPoints): void; } /** * Creates a new backend module for a given plugin. * * @public - * - * @remarks - * - * The `moduleId` should be equal to the module-specific prefix of the exported name, such - * that the full name is `moduleId + PluginId + "Module"`. For example, a GitHub entity - * provider module for the `catalog` plugin might have the module ID `'githubEntityProvider'`, - * and the full exported name would be `githubEntityProviderCatalogModule`. - * - * The `pluginId` should exactly match the `id` of the plugin that the module extends. + * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} */ export function createBackendModule<TOptions extends [options?: object] = []>( config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig), ): (...params: TOptions) => BackendFeature { - if (typeof config === 'function') { - return (...options: TOptions) => { - const c = config(...options); - return { - id: `${c.pluginId}.${c.moduleId}`, - register: c.register, - }; + const configCallback = typeof config === 'function' ? config : () => config; + return (...options: TOptions): InternalBackendFeature => { + const c = configCallback(...options); + + let registrations: InternalBackendModuleRegistration[]; + + return { + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations() { + if (registrations) { + return registrations; + } + let init: InternalBackendModuleRegistration['init'] | undefined = + undefined; + + c.register({ + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${c.moduleId} module for ${c.pluginId}`, + ); + } + + registrations = [ + { + type: 'module', + pluginId: c.pluginId, + moduleId: c.moduleId, + init, + }, + ]; + return registrations; + }, }; - } - return () => ({ - id: `${config.pluginId}.${config.moduleId}`, - register(register: BackendRegistrationPoints) { - // TODO: Hide registerExtensionPoint - return config.register(register); - }, - }); + }; } diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts index 0d5999ad12..242301ea4d 100644 --- a/packages/backend-plugin-api/src/wiring/index.ts +++ b/packages/backend-plugin-api/src/wiring/index.ts @@ -30,7 +30,8 @@ export { createExtensionPoint, } from './factories'; export type { - BackendRegistrationPoints, + BackendModuleRegistrationPoints, + BackendPluginRegistrationPoints, BackendFeature, ExtensionPoint, } from './types'; diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index cdb56ea2e4..c81ee3ceb7 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -32,15 +32,33 @@ export type ExtensionPoint<T> = { toString(): string; - $$ref: 'extension-point'; + $$type: '@backstage/ExtensionPoint'; }; -/** @public */ -export interface BackendRegistrationPoints { +/** + * The callbacks passed to the `register` method of a backend plugin. + * + * @public + */ +export interface BackendPluginRegistrationPoints { registerExtensionPoint<TExtensionPoint>( ref: ExtensionPoint<TExtensionPoint>, impl: TExtensionPoint, ): void; + registerInit<Deps extends { [name in string]: unknown }>(options: { + deps: { + [name in keyof Deps]: ServiceRef<Deps[name]>; + }; + init(deps: Deps): Promise<void>; + }): void; +} + +/** + * The callbacks passed to the `register` method of a backend module. + * + * @public + */ +export interface BackendModuleRegistrationPoints { registerInit<Deps extends { [name in string]: unknown }>(options: { deps: { [name in keyof Deps]: ServiceRef<Deps[name]> | ExtensionPoint<Deps[name]>; @@ -51,7 +69,36 @@ export interface BackendRegistrationPoints { /** @public */ export interface BackendFeature { - // TODO(Rugvip): Try to get rid of the ID at this level, allowing for a feature to register multiple features as a bundle - id: string; - register(reg: BackendRegistrationPoints): void; + // NOTE: This type is opaque in order to simplify future API evolution. + $$type: '@backstage/BackendFeature'; +} + +/** @internal */ +export interface InternalBackendFeature extends BackendFeature { + version: 'v1'; + getRegistrations(): Array< + InternalBackendPluginRegistration | InternalBackendModuleRegistration + >; +} + +/** @internal */ +export interface InternalBackendPluginRegistration { + pluginId: string; + type: 'plugin'; + extensionPoints: Array<readonly [ExtensionPoint<unknown>, unknown]>; + init: { + deps: Record<string, ServiceRef<unknown>>; + func(deps: Record<string, unknown>): Promise<void>; + }; +} + +/** @internal */ +export interface InternalBackendModuleRegistration { + pluginId: string; + moduleId: string; + type: 'module'; + init: { + deps: Record<string, ServiceRef<unknown> | ExtensionPoint<unknown>>; + func(deps: Record<string, unknown>): Promise<void>; + }; } diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 117dcd61e2..e70ff6146d 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/backend-tasks +## 0.4.4-next.0 + +### Patch Changes + +- f0685193ef: Added the adapted query to mysql and sqlite3 databases to not returning warning on logs +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.4.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.4.1 ### Patch Changes diff --git a/packages/backend-tasks/README.md b/packages/backend-tasks/README.md index b3c304eb34..afaca06e55 100644 --- a/packages/backend-tasks/README.md +++ b/packages/backend-tasks/README.md @@ -35,4 +35,4 @@ When working with the `@backstage/backend-tasks` library you may run into your t ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 6360835bdb..42d0f35374 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.4.1", + "version": "0.4.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts new file mode 100644 index 0000000000..5b5d592b7b --- /dev/null +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import { Knex } from 'knex'; +import { Duration } from 'luxon'; +import waitForExpect from 'wait-for-expect'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; +import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; +import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor'; + +const insertTask = async (knex: Knex, task: DbTasksRow) => { + return knex<DbTasksRow>(DB_TASKS_TABLE) + .insert(task) + .onConflict('id') + .merge(['settings_json']); +}; + +const getTask = async (knex: Knex): Promise<DbTasksRow> => { + return (await knex<DbTasksRow>(DB_TASKS_TABLE))[0]; +}; + +describe('PluginTaskSchedulerJanitor', () => { + const logger = getVoidLogger(); + const databases = TestDatabases.create({ + ids: [ + /* 'MYSQL_8' not supported yet */ + 'POSTGRES_13', + 'POSTGRES_9', + 'SQLITE_3', + ], + }); + + jest.setTimeout(60_000); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it.each(databases.eachSupportedId())( + 'Should update date if current_run_expires_at expires, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const dateYesterday = new Date( + new Date().setDate(new Date().getDate() - 1), + ); + + await insertTask(knex, { + id: 'task1', + settings_json: '', + next_run_start_at: new Date('2023-03-01 00:00:00'), + current_run_ticket: 'ticket', + current_run_started_at: dateYesterday, + current_run_expires_at: dateYesterday, + }); + + const worker = new PluginTaskSchedulerJanitor({ + waitBetweenRuns: Duration.fromObject({ milliseconds: 20 }), + knex, + logger, + }); + + const abortController = new AbortController(); + worker.start(abortController.signal); + + await waitForExpect(async () => { + await expect(getTask(knex)).resolves.toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + }); + + abortController.abort(); + }, + ); +}); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts index 81b0345c2c..f69b7d9b88 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts @@ -53,15 +53,33 @@ export class PluginTaskSchedulerJanitor { private async runOnce() { const dbNull = this.knex.raw('null'); + const configClient = this.knex.client.config.client; - const tasks = await this.knex<DbTasksRow>(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']); + let tasks: Array<{ id: string }>; + if (configClient.includes('sqlite3') || configClient.includes('mysql')) { + tasks = await this.knex<DbTasksRow>(DB_TASKS_TABLE) + .select('id') + .where('current_run_expires_at', '<', this.knex.fn.now()); + await this.knex<DbTasksRow>(DB_TASKS_TABLE) + .whereIn( + 'id', + tasks.map(t => t.id), + ) + .update({ + current_run_ticket: dbNull, + current_run_started_at: dbNull, + current_run_expires_at: dbNull, + }); + } else { + tasks = await this.knex<DbTasksRow>(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']); + } // In rare cases, knex drivers may ignore "returning", and return the number // of rows changed instead diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index ec52de68a4..8129bb3c0a 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,87 @@ # @backstage/backend-test-utils +## 0.1.35-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-app-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + +## 0.1.34 + +### Patch Changes + +- baf6e4c96a: Removed unnecessary `@backstage/cli` dependency. +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 1835311713: Added explicit return type signature for `mockServices.config()`. +- e716946103: Updated usage of the lifecycle service. +- 7e7557a2be: Updated the `mockServices.rootLogger` options to accept a single level option instead. +- 610d65e143: Updates to match new `BackendFeature` type. +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- b86efa2d04: Updated usage of `ServiceFactory`. +- 17b573e4be: The new backend system testing utilities have now been marked as stable API. +- f1adb2e36b: Removed the `ref` from all `mockServices`. +- d0901c9ba4: All mock service factories and mock service implementations are now available via the new experimental `mockServices` export. +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-app-api@0.4.0 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + +## 0.1.34-next.2 + +### Patch Changes + +- baf6e4c96a: Removed unnecessary `@backstage/cli` dependency. +- 1835311713: Added explicit return type signature for `mockServices.config()`. +- e716946103: Updated usage of the lifecycle service. +- 7e7557a2be: Updated the `mockServices.rootLogger` options to accept a single level option instead. +- 610d65e143: Updates to match new `BackendFeature` type. +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- f1adb2e36b: Removed the `ref` from all `mockServices`. +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.1.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.0 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-app-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## 0.1.34-next.0 + +### Patch Changes + +- d0901c9ba4: All mock service factories and mock service implementations are now available via the new experimental `mockServices` export. +- Updated dependencies + - @backstage/backend-app-api@0.3.2-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/cli@0.22.1 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.1.32 ### Patch Changes diff --git a/packages/backend-test-utils/README.md b/packages/backend-test-utils/README.md index 38756fa5d7..d0f52388bb 100644 --- a/packages/backend-test-utils/README.md +++ b/packages/backend-test-utils/README.md @@ -7,7 +7,7 @@ Test helpers library for Backstage backends. Add the library as a `devDependency` to your backend package: ```sh -# From the Backstage root directory, go to your backend package, or to a backend plugin +# From your Backstage root directory, go to your backend package, or to a backend plugin cd plugins/my-plugin-backend yarn add --dev @backstage/backend-test-utils ``` @@ -15,4 +15,4 @@ yarn add --dev @backstage/backend-test-utils ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 8ff29f8545..c13d1bb859 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -5,25 +5,119 @@ ```ts import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; +import { CacheService } from '@backstage/backend-plugin-api'; import { ConfigService } from '@backstage/backend-plugin-api'; +import { DatabaseService } from '@backstage/backend-plugin-api'; import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { HttpRouterFactoryOptions } from '@backstage/backend-app-api'; +import { HttpRouterService } from '@backstage/backend-plugin-api'; +import { IdentityService } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; import { Knex } from 'knex'; +import { LifecycleService } from '@backstage/backend-plugin-api'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { PermissionsService } from '@backstage/backend-plugin-api'; +import { RootLifecycleService } from '@backstage/backend-plugin-api'; +import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; +import { TokenManagerService } from '@backstage/backend-plugin-api'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public (undocumented) export function isDockerDisabledForTests(): boolean; -// @alpha (undocumented) -export const mockConfigFactory: ( - options?: - | { - data?: JsonObject | undefined; - } - | undefined, -) => ServiceFactory<ConfigService>; +// @public (undocumented) +export namespace mockServices { + // (undocumented) + export namespace cache { + const // (undocumented) + factory: () => ServiceFactory<CacheService, 'plugin'>; + } + // (undocumented) + export function config(options?: config.Options): ConfigService; + // (undocumented) + export namespace config { + // (undocumented) + export type Options = { + data?: JsonObject; + }; + const // (undocumented) + factory: ( + options?: Options | undefined, + ) => ServiceFactory<ConfigService, 'root'>; + } + // (undocumented) + export namespace database { + const // (undocumented) + factory: () => ServiceFactory<DatabaseService, 'plugin'>; + } + // (undocumented) + export namespace httpRouter { + const // (undocumented) + factory: ( + options?: HttpRouterFactoryOptions | undefined, + ) => ServiceFactory<HttpRouterService, 'plugin'>; + } + // (undocumented) + export function identity(): IdentityService; + // (undocumented) + export namespace identity { + const // (undocumented) + factory: () => ServiceFactory<IdentityService, 'plugin'>; + } + // (undocumented) + export namespace lifecycle { + const // (undocumented) + factory: () => ServiceFactory<LifecycleService, 'plugin'>; + } + // (undocumented) + export namespace logger { + const // (undocumented) + factory: () => ServiceFactory<LoggerService, 'plugin'>; + } + // (undocumented) + export namespace permissions { + const // (undocumented) + factory: () => ServiceFactory<PermissionsService, 'plugin'>; + } + // (undocumented) + export namespace rootLifecycle { + const // (undocumented) + factory: () => ServiceFactory<RootLifecycleService, 'root'>; + } + // (undocumented) + export function rootLogger(options?: rootLogger.Options): LoggerService; + // (undocumented) + export namespace rootLogger { + // (undocumented) + export type Options = { + level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; + }; + const // (undocumented) + factory: ( + options?: Options | undefined, + ) => ServiceFactory<LoggerService, 'root'>; + } + // (undocumented) + export namespace scheduler { + const // (undocumented) + factory: () => ServiceFactory<SchedulerService, 'plugin'>; + } + // (undocumented) + export function tokenManager(): TokenManagerService; + // (undocumented) + export namespace tokenManager { + const // (undocumented) + factory: () => ServiceFactory<TokenManagerService, 'plugin'>; + } + // (undocumented) + export namespace urlReader { + const // (undocumented) + factory: () => ServiceFactory<UrlReaderService, 'plugin'>; + } +} // @public export function setupRequestMockHandlers(worker: { @@ -32,7 +126,7 @@ export function setupRequestMockHandlers(worker: { resetHandlers: () => void; }): void; -// @alpha (undocumented) +// @public (undocumented) export function startTestBackend< TServices extends any[], TExtensionPoints extends any[], @@ -40,12 +134,12 @@ export function startTestBackend< options: TestBackendOptions<TServices, TExtensionPoints>, ): Promise<TestBackend>; -// @alpha (undocumented) +// @public (undocumented) export interface TestBackend extends Backend { readonly server: ExtendedHttpServer; } -// @alpha (undocumented) +// @public (undocumented) export interface TestBackendOptions< TServices extends any[], TExtensionPoints extends any[], diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 5a4c26ba49..73f0e9762d 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,14 +1,22 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.32", + "version": "0.1.35-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "node-library" @@ -25,7 +33,7 @@ ], "license": "Apache-2.0", "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -37,14 +45,13 @@ "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", - "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", "better-sqlite3": "^8.0.0", "express": "^4.17.1", "knex": "^2.0.0", - "msw": "^0.49.0", + "msw": "^1.0.0", "mysql2": "^2.2.5", "pg": "^8.3.0", "testcontainers": "^8.1.2", @@ -56,7 +63,6 @@ "supertest": "^6.1.3" }, "files": [ - "dist", - "alpha" + "dist" ] } diff --git a/packages/backend-test-utils/src/next/implementations/mockRootLoggerService.ts b/packages/backend-test-utils/src/next/implementations/mockRootLoggerService.ts deleted file mode 100644 index bb3af6a9fb..0000000000 --- a/packages/backend-test-utils/src/next/implementations/mockRootLoggerService.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - coreServices, - createServiceFactory, - LoggerService, - LogMeta, - RootLoggerService, -} from '@backstage/backend-plugin-api'; - -interface MockLoggerOptions { - levels: - | boolean - | { error: boolean; warn: boolean; info: boolean; debug: boolean }; -} - -class MockLogger implements RootLoggerService { - #levels: Exclude<MockLoggerOptions['levels'], boolean>; - #meta: LogMeta; - - error(message: string, meta?: LogMeta | Error | undefined): void { - this.#log('error', message, meta); - } - - warn(message: string, meta?: LogMeta | Error | undefined): void { - this.#log('warn', message, meta); - } - - info(message: string, meta?: LogMeta | Error | undefined): void { - this.#log('info', message, meta); - } - - debug(message: string, meta?: LogMeta | Error | undefined): void { - this.#log('debug', message, meta); - } - - child(meta: LogMeta): LoggerService { - return new MockLogger(this.#levels, { ...this.#meta, ...meta }); - } - - constructor(levels: MockLoggerOptions['levels'], meta: LogMeta) { - if (typeof levels === 'boolean') { - this.#levels = { - error: levels, - debug: levels, - info: levels, - warn: levels, - }; - } else { - this.#levels = levels; - } - this.#meta = meta; - } - - #log( - level: 'error' | 'warn' | 'info' | 'debug', - message: string, - meta?: LogMeta | Error | undefined, - ) { - if (this.#levels[level]) { - const labels = Object.entries(this.#meta) - .map(([key, value]) => `${key}=${value}`) - .join(','); - console[level](`${labels} ${message}`, meta); - } - } -} - -/** @alpha */ -export const mockRootLoggerService = createServiceFactory({ - service: coreServices.rootLogger, - deps: {}, - async factory(_deps) { - return new MockLogger(false, {}); - }, -}); diff --git a/packages/backend-test-utils/src/next/index.ts b/packages/backend-test-utils/src/next/index.ts index 9f9edfd837..572fb3a9e7 100644 --- a/packages/backend-test-utils/src/next/index.ts +++ b/packages/backend-test-utils/src/next/index.ts @@ -15,4 +15,4 @@ */ export * from './wiring'; -export * from './implementations'; +export * from './services'; diff --git a/packages/backend-test-utils/src/next/implementations/mockIdentityService.ts b/packages/backend-test-utils/src/next/services/MockIdentityService.ts similarity index 75% rename from packages/backend-test-utils/src/next/implementations/mockIdentityService.ts rename to packages/backend-test-utils/src/next/services/MockIdentityService.ts index a838c8c879..ff424f79c2 100644 --- a/packages/backend-test-utils/src/next/implementations/mockIdentityService.ts +++ b/packages/backend-test-utils/src/next/services/MockIdentityService.ts @@ -13,17 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - coreServices, - createServiceFactory, - IdentityService, -} from '@backstage/backend-plugin-api'; +import { IdentityService } from '@backstage/backend-plugin-api'; import { IdentityApiGetIdentityRequest, BackstageIdentityResponse, } from '@backstage/plugin-auth-node'; -class MockIdentityServiceImpl implements IdentityService { +export class MockIdentityService implements IdentityService { getIdentity( _options: IdentityApiGetIdentityRequest, ): Promise<BackstageIdentityResponse | undefined> { @@ -37,11 +33,3 @@ class MockIdentityServiceImpl implements IdentityService { }); } } - -export const mockIdentityFactory = createServiceFactory({ - service: coreServices.identity, - deps: {}, - async factory() { - return new MockIdentityServiceImpl(); - }, -}); diff --git a/packages/backend-test-utils/src/next/services/MockRootLoggerService.test.ts b/packages/backend-test-utils/src/next/services/MockRootLoggerService.test.ts new file mode 100644 index 0000000000..a3cbb5c384 --- /dev/null +++ b/packages/backend-test-utils/src/next/services/MockRootLoggerService.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MockRootLoggerService } from './MockRootLoggerService'; + +describe('MockRootLoggerService', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should be silent by default', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create(); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set none level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'none' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set error level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'error' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set warn level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'warn' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set info level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'info' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + expect(console.info).toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set debug level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'debug' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + expect(console.info).toHaveBeenCalled(); + expect(console.debug).toHaveBeenCalled(); + }); +}); diff --git a/packages/backend-test-utils/src/next/services/MockRootLoggerService.ts b/packages/backend-test-utils/src/next/services/MockRootLoggerService.ts new file mode 100644 index 0000000000..8b973eeeab --- /dev/null +++ b/packages/backend-test-utils/src/next/services/MockRootLoggerService.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + LoggerService, + RootLoggerService, +} from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; +import type { mockServices } from './mockServices'; + +const levels = { + none: 0, + error: 1, + warn: 2, + info: 3, + debug: 4, +}; + +export class MockRootLoggerService implements RootLoggerService { + #level: number; + #meta: JsonObject; + + static create( + options?: mockServices.rootLogger.Options, + ): MockRootLoggerService { + const level = options?.level ?? 'none'; + if (!(level in levels)) { + throw new Error(`Invalid log level '${level}'`); + } + return new MockRootLoggerService(levels[level], {}); + } + + error(message: string, meta?: JsonObject | Error | undefined): void { + this.#log('error', message, meta); + } + + warn(message: string, meta?: JsonObject | Error | undefined): void { + this.#log('warn', message, meta); + } + + info(message: string, meta?: JsonObject | Error | undefined): void { + this.#log('info', message, meta); + } + + debug(message: string, meta?: JsonObject | Error | undefined): void { + this.#log('debug', message, meta); + } + + child(meta: JsonObject): LoggerService { + return new MockRootLoggerService(this.#level, { ...this.#meta, ...meta }); + } + + private constructor(level: number, meta: JsonObject) { + this.#level = level; + this.#meta = meta; + } + + #log( + level: 'error' | 'warn' | 'info' | 'debug', + message: string, + meta?: JsonObject | Error | undefined, + ) { + const levelValue = levels[level] ?? 0; + if (levelValue <= this.#level) { + const labels = Object.entries(this.#meta) + .map(([key, value]) => `${key}=${value}`) + .join(','); + console[level](`${labels} ${message}`, meta); + } + } +} diff --git a/packages/backend-test-utils/src/next/implementations/index.ts b/packages/backend-test-utils/src/next/services/index.ts similarity index 91% rename from packages/backend-test-utils/src/next/implementations/index.ts rename to packages/backend-test-utils/src/next/services/index.ts index 55f417f8df..f562150efa 100644 --- a/packages/backend-test-utils/src/next/implementations/index.ts +++ b/packages/backend-test-utils/src/next/services/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { mockConfigFactory } from './mockConfigService'; +export { mockServices } from './mockServices'; diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts new file mode 100644 index 0000000000..8a3125214c --- /dev/null +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -0,0 +1,140 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ConfigService, + coreServices, + createServiceFactory, + IdentityService, + LoggerService, + ServiceFactory, + ServiceRef, + TokenManagerService, +} from '@backstage/backend-plugin-api'; +import { + cacheServiceFactory, + databaseServiceFactory, + httpRouterServiceFactory, + lifecycleServiceFactory, + loggerServiceFactory, + permissionsServiceFactory, + rootLifecycleServiceFactory, + schedulerServiceFactory, + urlReaderServiceFactory, +} from '@backstage/backend-app-api'; +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { MockIdentityService } from './MockIdentityService'; +import { MockRootLoggerService } from './MockRootLoggerService'; + +function simpleFactory< + TService, + TScope extends 'root' | 'plugin', + TOptions extends [options?: object] = [], +>( + ref: ServiceRef<TService, TScope>, + factory: (...options: TOptions) => TService, +): (...options: TOptions) => ServiceFactory<TService, TScope> { + return createServiceFactory((options: unknown) => ({ + service: ref as ServiceRef<TService, any>, + deps: {}, + async factory() { + return (factory as any)(options); + }, + })) as (...options: TOptions) => ServiceFactory<TService, any>; +} + +/** + * @public + */ +export namespace mockServices { + export function config(options?: config.Options): ConfigService { + return new ConfigReader(options?.data, 'mock-config'); + } + export namespace config { + export type Options = { data?: JsonObject }; + + export const factory = simpleFactory(coreServices.config, config); + } + + export function rootLogger(options?: rootLogger.Options): LoggerService { + return MockRootLoggerService.create(options); + } + export namespace rootLogger { + export type Options = { + level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; + }; + + export const factory = simpleFactory(coreServices.rootLogger, rootLogger); + } + + export function tokenManager(): TokenManagerService { + return { + async getToken(): Promise<{ token: string }> { + return { token: 'mock-token' }; + }, + async authenticate(token: string): Promise<void> { + if (token !== 'mock-token') { + throw new Error('Invalid token'); + } + }, + }; + } + export namespace tokenManager { + export const factory = simpleFactory( + coreServices.tokenManager, + tokenManager, + ); + } + + export function identity(): IdentityService { + return new MockIdentityService(); + } + export namespace identity { + export const factory = simpleFactory(coreServices.identity, identity); + } + + // TODO(Rugvip): Not all core services have implementations available here yet. + // some may need a bit more refactoring for it to be simpler to + // re-implement functioning mock versions here. + export namespace cache { + export const factory = cacheServiceFactory; + } + export namespace database { + export const factory = databaseServiceFactory; + } + export namespace httpRouter { + export const factory = httpRouterServiceFactory; + } + export namespace lifecycle { + export const factory = lifecycleServiceFactory; + } + export namespace logger { + export const factory = loggerServiceFactory; + } + export namespace permissions { + export const factory = permissionsServiceFactory; + } + export namespace rootLifecycle { + export const factory = rootLifecycleServiceFactory; + } + export namespace scheduler { + export const factory = schedulerServiceFactory; + } + export namespace urlReader { + export const factory = urlReaderServiceFactory; + } +} diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts index dca463a9ae..aa93a7dae1 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -40,10 +40,8 @@ beforeAll(async () => { env.registerInit({ deps: { lifecycle: coreServices.lifecycle }, async init({ lifecycle }) { - lifecycle.addShutdownHook({ - fn() { - globalTestBackendHasBeenStopped = true; - }, + lifecycle.addShutdownHook(() => { + globalTestBackendHasBeenStopped = true; }); }, }); @@ -145,7 +143,7 @@ describe('TestBackend', () => { lifecycle: coreServices.lifecycle, }, async init({ lifecycle }) { - lifecycle.addShutdownHook({ fn: shutdownSpy }); + lifecycle.addShutdownHook(shutdownSpy); }, }); }, @@ -165,7 +163,7 @@ describe('TestBackend', () => { expect.assertions(2); const testPlugin = createBackendPlugin({ - id: 'test', + pluginId: 'test', register(env) { env.registerInit({ deps: { @@ -201,7 +199,7 @@ describe('TestBackend', () => { it('should allow making requests via supertest', async () => { const testPlugin = createBackendPlugin({ - id: 'test', + pluginId: 'test', register(env) { env.registerInit({ deps: { diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 78c2f78e5e..66bbb62193 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -17,15 +17,6 @@ import { Backend, createSpecializedBackend, - lifecycleFactory, - rootLifecycleFactory, - loggerFactory, - cacheFactory, - permissionsFactory, - schedulerFactory, - urlReaderFactory, - databaseFactory, - httpRouterFactory, MiddlewareFactory, createHttpServer, ExtendedHttpServer, @@ -39,16 +30,13 @@ import { BackendFeature, ExtensionPoint, coreServices, + createBackendPlugin, } from '@backstage/backend-plugin-api'; - -import { mockConfigFactory } from '../implementations/mockConfigService'; -import { mockRootLoggerService } from '../implementations/mockRootLoggerService'; -import { mockTokenManagerFactory } from '../implementations/mockTokenManagerService'; +import { mockServices } from '../services'; import { ConfigReader } from '@backstage/config'; import express from 'express'; -import { mockIdentityFactory } from '../implementations/mockIdentityService'; -/** @alpha */ +/** @public */ export interface TestBackendOptions< TServices extends any[], TExtensionPoints extends any[], @@ -72,7 +60,7 @@ export interface TestBackendOptions< features?: BackendFeature[]; } -/** @alpha */ +/** @public */ export interface TestBackend extends Backend { /** * Provides access to the underling HTTP server for use with utilities @@ -84,24 +72,24 @@ export interface TestBackend extends Backend { } const defaultServiceFactories = [ - cacheFactory(), - databaseFactory(), - httpRouterFactory(), - lifecycleFactory(), - loggerFactory(), - mockConfigFactory(), - mockRootLoggerService(), - mockIdentityFactory(), - mockTokenManagerFactory(), - permissionsFactory(), - rootLifecycleFactory(), - schedulerFactory(), - urlReaderFactory(), + mockServices.cache.factory(), + mockServices.config.factory(), + mockServices.database.factory(), + mockServices.httpRouter.factory(), + mockServices.identity.factory(), + mockServices.lifecycle.factory(), + mockServices.logger.factory(), + mockServices.permissions.factory(), + mockServices.rootLifecycle.factory(), + mockServices.rootLogger.factory(), + mockServices.scheduler.factory(), + mockServices.tokenManager.factory(), + mockServices.urlReader.factory(), ]; const backendInstancesToCleanUp = new Array<Backend>(); -/** @alpha */ +/** @public */ export async function startTestBackend< TServices extends any[], TExtensionPoints extends any[], @@ -142,12 +130,7 @@ export async function startTestBackend< { logger }, ); - lifecycle.addShutdownHook({ - async fn() { - await server.stop(); - }, - logger, - }); + lifecycle.addShutdownHook(() => server.stop(), { logger }); await server.start(); @@ -211,16 +194,18 @@ export async function startTestBackend< backendInstancesToCleanUp.push(backend); - backend.add({ - id: `---test-extension-point-registrar`, - register(reg) { - for (const [ref, impl] of extensionPoints) { - reg.registerExtensionPoint(ref, impl); - } + backend.add( + createBackendPlugin({ + pluginId: `---test-extension-point-registrar`, + register(reg) { + for (const [ref, impl] of extensionPoints) { + reg.registerExtensionPoint(ref, impl); + } - reg.registerInit({ deps: {}, async init() {} }); - }, - }); + reg.registerInit({ deps: {}, async init() {} }); + }, + })(), + ); for (const feature of features) { backend.add(feature); diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index c482402ebd..a9551d3c47 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,248 @@ # example-backend +## 0.2.81-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-todo-backend@0.1.40-next.0 + - @backstage/plugin-scaffolder-backend@1.11.1-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/plugin-tech-insights-backend@0.5.9-next.0 + - @backstage/plugin-linguist-backend@0.2.0-next.0 + - @backstage/plugin-adr-backend@0.3.1-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/plugin-techdocs-backend@1.5.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-events-backend@0.2.4-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/plugin-app-backend@0.3.43-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/plugin-proxy-backend@0.2.37-next.0 + - @backstage/plugin-auth-backend@0.18.1-next.0 + - @backstage/plugin-badges-backend@0.1.37-next.0 + - @backstage/plugin-code-coverage-backend@0.2.9-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.1-next.0 + - @backstage/plugin-jenkins-backend@0.1.33-next.0 + - @backstage/plugin-kubernetes-backend@0.9.4-next.0 + - @backstage/plugin-lighthouse-backend@0.1.1-next.0 + - @backstage/plugin-playlist-backend@0.2.6-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.11-next.0 + - example-app@0.2.81-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-azure-devops-backend@0.3.22-next.0 + - @backstage/plugin-azure-sites-backend@0.1.5-next.0 + - @backstage/plugin-explore-backend@0.0.5-next.0 + - @backstage/plugin-graphql-backend@0.1.33-next.0 + - @backstage/plugin-kafka-backend@0.2.36-next.0 + - @backstage/plugin-permission-backend@0.5.18-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + - @backstage/plugin-rollbar-backend@0.1.40-next.0 + - @backstage/plugin-search-backend@1.2.4-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.4-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.4-next.0 + - @backstage/plugin-search-backend-node@1.1.4-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.27-next.0 + - @backstage/plugin-tech-insights-node@0.4.1-next.0 + +## 0.2.80 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/plugin-playlist-backend@0.2.5 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3 + - @backstage/backend-common@0.18.2 + - @backstage/plugin-kubernetes-backend@0.9.3 + - @backstage/plugin-lighthouse-backend@0.1.0 + - @backstage/plugin-code-coverage-backend@0.2.8 + - @backstage/plugin-azure-devops-backend@0.3.21 + - @backstage/plugin-azure-sites-backend@0.1.4 + - @backstage/plugin-adr-backend@0.3.0 + - @backstage/plugin-tech-insights-backend@0.5.8 + - @backstage/plugin-tech-insights-node@0.4.0 + - @backstage/plugin-entity-feedback-backend@0.1.0 + - @backstage/plugin-search-backend@1.2.3 + - @backstage/plugin-techdocs-backend@1.5.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26 + - @backstage/plugin-scaffolder-backend@1.11.0 + - @backstage/plugin-events-backend@0.2.3 + - @backstage/plugin-kafka-backend@0.2.35 + - @backstage/plugin-proxy-backend@0.2.36 + - @backstage/plugin-app-backend@0.3.42 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-auth-backend@0.18.0 + - @backstage/plugin-linguist-backend@0.1.0 + - @backstage/plugin-events-node@0.2.3 + - example-app@0.2.80 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-badges-backend@0.1.36 + - @backstage/plugin-explore-backend@0.0.4 + - @backstage/plugin-graphql-backend@0.1.32 + - @backstage/plugin-jenkins-backend@0.1.32 + - @backstage/plugin-permission-backend@0.5.17 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + - @backstage/plugin-rollbar-backend@0.1.39 + - @backstage/plugin-search-backend-module-pg@0.5.3 + - @backstage/plugin-search-backend-node@1.1.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-todo-backend@0.1.39 + +## 0.2.80-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-backend@0.1.0-next.0 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/plugin-search-backend@1.2.3-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-events-backend@0.2.3-next.2 + - @backstage/plugin-kafka-backend@0.2.35-next.2 + - @backstage/plugin-proxy-backend@0.2.36-next.2 + - @backstage/plugin-app-backend@0.3.42-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-kubernetes-backend@0.9.3-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-backend@0.17.5-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/plugin-playlist-backend@0.2.5-next.2 + - @backstage/plugin-rollbar-backend@0.1.39-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.3-next.2 + - @backstage/plugin-tech-insights-backend@0.5.8-next.2 + - @backstage/plugin-techdocs-backend@1.5.3-next.2 + - example-app@0.2.80-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-backend@0.2.7-next.2 + - @backstage/plugin-azure-devops-backend@0.3.21-next.2 + - @backstage/plugin-azure-sites-backend@0.1.4-next.2 + - @backstage/plugin-badges-backend@0.1.36-next.2 + - @backstage/plugin-code-coverage-backend@0.2.8-next.2 + - @backstage/plugin-explore-backend@0.0.4-next.2 + - @backstage/plugin-graphql-backend@0.1.32-next.2 + - @backstage/plugin-jenkins-backend@0.1.32-next.2 + - @backstage/plugin-permission-backend@0.5.17-next.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.2 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.2 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + - @backstage/plugin-todo-backend@0.1.39-next.2 + +## 0.2.80-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/plugin-tech-insights-backend@0.5.8-next.1 + - @backstage/plugin-tech-insights-node@0.4.0-next.1 + - @backstage/plugin-azure-devops-backend@0.3.21-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/plugin-kubernetes-backend@0.9.3-next.1 + - @backstage/plugin-scaffolder-backend@1.11.0-next.1 + - @backstage/plugin-playlist-backend@0.2.5-next.1 + - example-app@0.2.80-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-backend@0.2.7-next.1 + - @backstage/plugin-app-backend@0.3.42-next.1 + - @backstage/plugin-auth-backend@0.17.5-next.1 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-azure-sites-backend@0.1.4-next.1 + - @backstage/plugin-badges-backend@0.1.36-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-code-coverage-backend@0.2.8-next.1 + - @backstage/plugin-events-backend@0.2.3-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + - @backstage/plugin-explore-backend@0.0.4-next.1 + - @backstage/plugin-graphql-backend@0.1.32-next.1 + - @backstage/plugin-jenkins-backend@0.1.32-next.1 + - @backstage/plugin-kafka-backend@0.2.35-next.1 + - @backstage/plugin-permission-backend@0.5.17-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + - @backstage/plugin-proxy-backend@0.2.36-next.1 + - @backstage/plugin-rollbar-backend@0.1.39-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.1 + - @backstage/plugin-search-backend@1.2.3-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.1 + - @backstage/plugin-search-backend-module-pg@0.5.3-next.1 + - @backstage/plugin-search-backend-node@1.1.3-next.1 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.1 + - @backstage/plugin-techdocs-backend@1.5.3-next.1 + - @backstage/plugin-todo-backend@0.1.39-next.1 + +## 0.2.80-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-backend@0.9.3-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.0 + - @backstage/plugin-scaffolder-backend@1.11.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - example-app@0.2.80-next.0 + - @backstage/plugin-techdocs-backend@1.5.3-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-app-backend@0.3.42-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-adr-backend@0.2.7-next.0 + - @backstage/plugin-auth-backend@0.17.5-next.0 + - @backstage/plugin-badges-backend@0.1.36-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/plugin-code-coverage-backend@0.2.8-next.0 + - @backstage/plugin-jenkins-backend@0.1.32-next.0 + - @backstage/plugin-kafka-backend@0.2.35-next.0 + - @backstage/plugin-playlist-backend@0.2.5-next.0 + - @backstage/plugin-tech-insights-backend@0.5.8-next.0 + - @backstage/plugin-todo-backend@0.1.39-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-events-backend@0.2.3-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + - @backstage/plugin-rollbar-backend@0.1.39-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.3-next.0 + - @backstage/plugin-azure-devops-backend@0.3.21-next.0 + - @backstage/plugin-azure-sites-backend@0.1.4-next.0 + - @backstage/plugin-explore-backend@0.0.4-next.0 + - @backstage/plugin-graphql-backend@0.1.32-next.0 + - @backstage/plugin-permission-backend@0.5.17-next.0 + - @backstage/plugin-proxy-backend@0.2.36-next.0 + - @backstage/plugin-search-backend@1.2.3-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.0 + - @backstage/plugin-search-backend-node@1.1.3-next.0 + - @backstage/plugin-tech-insights-node@0.3.10-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.2.79 ### Patch Changes diff --git a/packages/backend/README.md b/packages/backend/README.md index e4fd81749d..f86d09842a 100644 --- a/packages/backend/README.md +++ b/packages/backend/README.md @@ -58,4 +58,4 @@ Read more about the [auth-backend](https://github.com/backstage/backstage/blob/m ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/backend/package.json b/packages/backend/package.json index e3240f8cff..2e742b365d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.79", + "version": "0.2.81-next.0", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,6 +42,7 @@ "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-code-coverage-backend": "workspace:^", + "@backstage/plugin-entity-feedback-backend": "workspace:^", "@backstage/plugin-events-backend": "workspace:^", "@backstage/plugin-events-node": "workspace:^", "@backstage/plugin-explore-backend": "workspace:^", @@ -49,6 +50,8 @@ "@backstage/plugin-jenkins-backend": "workspace:^", "@backstage/plugin-kafka-backend": "workspace:^", "@backstage/plugin-kubernetes-backend": "workspace:^", + "@backstage/plugin-lighthouse-backend": "workspace:^", + "@backstage/plugin-linguist-backend": "workspace:^", "@backstage/plugin-permission-backend": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a721c443d6..011be941cf 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -44,6 +44,7 @@ import azureDevOps from './plugins/azure-devops'; import catalog from './plugins/catalog'; import catalogEventBasedProviders from './plugins/catalogEventBasedProviders'; import codeCoverage from './plugins/codecoverage'; +import entityFeedback from './plugins/entityFeedback'; import events from './plugins/events'; import explore from './plugins/explore'; import kubernetes from './plugins/kubernetes'; @@ -62,6 +63,8 @@ import jenkins from './plugins/jenkins'; import permission from './plugins/permission'; import playlist from './plugins/playlist'; import adr from './plugins/adr'; +import lighthouse from './plugins/lighthouse'; +import linguist from './plugins/linguist'; import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; @@ -146,12 +149,17 @@ async function main() { ); const permissionEnv = useHotMemoize(module, () => createEnv('permission')); const playlistEnv = useHotMemoize(module, () => createEnv('playlist')); + const entityFeedbackEnv = useHotMemoize(module, () => + createEnv('entityFeedback'), + ); const eventsEnv = useHotMemoize(module, () => createEnv('events')); const exploreEnv = useHotMemoize(module, () => createEnv('explore')); + const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse')); const eventBasedEntityProviders = await catalogEventBasedProviders( catalogEnv, ); + const linguistEnv = useHotMemoize(module, () => createEnv('linguist')); const apiRouter = Router(); apiRouter.use( @@ -177,9 +185,13 @@ async function main() { apiRouter.use('/permission', await permission(permissionEnv)); apiRouter.use('/playlist', await playlist(playlistEnv)); apiRouter.use('/explore', await explore(exploreEnv)); + apiRouter.use('/entity-feedback', await entityFeedback(entityFeedbackEnv)); apiRouter.use('/adr', await adr(adrEnv)); + apiRouter.use('/linguist', await linguist(linguistEnv)); apiRouter.use(notFoundHandler()); + await lighthouse(lighthouseEnv); + const service = createServiceBuilder(module) .loadConfig(config) .addRouter('', await healthcheck(healthcheckEnv)) diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 8beda6f3fb..0d92315f92 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -114,6 +114,13 @@ export default async function createPlugin( }, }), + bitbucketServer: providers.bitbucketServer.create({ + signIn: { + resolver: + providers.bitbucketServer.resolvers.emailMatchingUserEntityProfileEmail(), + }, + }), + // This is an example of how to configure the OAuth2Proxy provider as well // as how to sign a user in without a matching user entity in the catalog. // You can try it out using `<ProxiedSignInPage {...props} provider="myproxy" />` diff --git a/packages/backend/src/plugins/entityFeedback.ts b/packages/backend/src/plugins/entityFeedback.ts new file mode 100644 index 0000000000..c37c3c9a97 --- /dev/null +++ b/packages/backend/src/plugins/entityFeedback.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRouter } from '@backstage/plugin-entity-feedback-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default function createPlugin(env: PluginEnvironment): Promise<Router> { + return createRouter({ + database: env.database, + discovery: env.discovery, + identity: env.identity, + logger: env.logger, + }); +} diff --git a/packages/backend/src/plugins/lighthouse.ts b/packages/backend/src/plugins/lighthouse.ts new file mode 100644 index 0000000000..5e805b7b80 --- /dev/null +++ b/packages/backend/src/plugins/lighthouse.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createScheduler } from '@backstage/plugin-lighthouse-backend'; +import { PluginEnvironment } from '../types'; +import { CatalogClient } from '@backstage/catalog-client'; + +export default async function createPlugin(env: PluginEnvironment) { + const { logger, scheduler, config } = env; + + const catalogClient = new CatalogClient({ + discoveryApi: env.discovery, + }); + + await createScheduler({ logger, scheduler, config, catalogClient }); +} diff --git a/packages/backend/src/plugins/linguist.ts b/packages/backend/src/plugins/linguist.ts new file mode 100644 index 0000000000..2c8713a7e3 --- /dev/null +++ b/packages/backend/src/plugins/linguist.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; +import { createRouter } from '@backstage/plugin-linguist-backend'; +import { Router } from 'express'; +import type { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise<Router> { + const schedule: TaskScheduleDefinition = { + frequency: { minutes: 2 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 15 }, + }; + + return createRouter( + { + schedule: schedule, + age: { days: 30 }, + batchSize: 2, + useSourceLocation: false, + }, + { ...env }, + ); +} diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 879480316d..99b07f3c5c 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/catalog-client +## 1.4.0-next.0 + +### Minor Changes + +- 8c030b1826: Add `queryEntities` method to `CatalogApi`. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/errors@1.1.4 + +## 1.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/errors@1.1.4 + +## 1.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/errors@1.1.4 + +## 1.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + ## 1.3.0 ### Minor Changes diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index a81314308b..22feceabec 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -58,6 +58,10 @@ export interface CatalogApi { locationRef: string, options?: CatalogRequestOptions, ): Promise<Location_2 | undefined>; + queryEntities( + request?: QueryEntitiesRequest, + options?: CatalogRequestOptions, + ): Promise<QueryEntitiesResponse>; refreshEntity( entityRef: string, options?: CatalogRequestOptions, @@ -124,6 +128,10 @@ export class CatalogClient implements CatalogApi { locationRef: string, options?: CatalogRequestOptions, ): Promise<Location_2 | undefined>; + queryEntities( + request?: QueryEntitiesRequest, + options?: CatalogRequestOptions, + ): Promise<QueryEntitiesResponse>; refreshEntity( entityRef: string, options?: CatalogRequestOptions, @@ -219,10 +227,7 @@ export interface GetEntityAncestorsResponse { // @public export interface GetEntityFacetsRequest { facets: string[]; - filter?: - | Record<string, string | symbol | (string | symbol)[]>[] - | Record<string, string | symbol | (string | symbol)[]> - | undefined; + filter?: EntityFilterQuery; } // @public @@ -244,6 +249,40 @@ type Location_2 = { }; export { Location_2 as Location }; +// @public +export type QueryEntitiesCursorRequest = { + fields?: string[]; + limit?: number; + cursor: string; +}; + +// @public +export type QueryEntitiesInitialRequest = { + fields?: string[]; + limit?: number; + filter?: EntityFilterQuery; + orderFields?: EntityOrderQuery; + fullTextFilter?: { + term: string; + fields?: string[]; + }; +}; + +// @public +export type QueryEntitiesRequest = + | QueryEntitiesInitialRequest + | QueryEntitiesCursorRequest; + +// @public +export type QueryEntitiesResponse = { + items: Entity[]; + totalItems: number; + pageInfo: { + nextCursor?: string; + prevCursor?: string; + }; +}; + // @public export type ValidateEntityResponse = | { diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index b8d3b29ee7..91e852c3b0 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": "1.3.0", + "version": "1.4.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,7 +38,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 2e9d2a0679..c235ff1592 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -18,7 +18,11 @@ import { Entity } from '@backstage/catalog-model'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { CatalogClient } from './CatalogClient'; -import { CATALOG_FILTER_EXISTS, GetEntitiesResponse } from './types/api'; +import { + CATALOG_FILTER_EXISTS, + GetEntitiesResponse, + QueryEntitiesResponse, +} from './types/api'; import { DiscoveryApi } from './types/discovery'; const server = setupServer(); @@ -249,6 +253,185 @@ describe('CatalogClient', () => { }); }); + describe('queryEntities', () => { + const defaultResponse: QueryEntitiesResponse = { + items: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test2', + namespace: 'test1', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test1', + namespace: 'test1', + }, + }, + ], + pageInfo: { + nextCursor: 'next', + prevCursor: 'prev', + }, + totalItems: 10, + }; + + beforeEach(() => { + server.use( + rest.get(`${mockBaseUrl}/entities/by-query`, (_, res, ctx) => { + return res(ctx.json(defaultResponse)); + }), + ); + }); + + it('should fetch entities from correct endpoint', async () => { + const response = await client.queryEntities({}, { token }); + expect(response?.items).toEqual(defaultResponse.items); + expect(response?.totalItems).toEqual(defaultResponse.totalItems); + expect(response?.pageInfo.nextCursor).toBeDefined(); + expect(response?.pageInfo.prevCursor).toBeDefined(); + }); + + it('builds multiple entity search filters properly', async () => { + const mockedEndpoint = jest + .fn() + .mockImplementation((_req, res, ctx) => + res(ctx.json({ items: [], totalItems: 0 })), + ); + + server.use(rest.get(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + const response = await client.queryEntities( + { + filter: [ + { + a: '1', + b: ['2', '3'], + ö: '=', + }, + { + a: '2', + }, + { + c: CATALOG_FILTER_EXISTS, + }, + ], + }, + { token }, + ); + + expect(response).toEqual({ items: [], totalItems: 0 }); + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + expect(mockedEndpoint.mock.calls[0][0].url.search).toBe( + '?filter=a=1,b=2,b=3,%C3%B6=%3D&filter=a=2&filter=c', + ); + }); + + it('should send query params correctly on initial request', async () => { + const mockedEndpoint = jest + .fn() + .mockImplementation((_req, res, ctx) => + res(ctx.json({ items: [], totalItems: 0 })), + ); + + server.use(rest.get(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + fields: ['a', 'b'], + limit: 100, + fullTextFilter: { + term: 'query', + }, + orderFields: [ + { field: 'metadata.name', order: 'asc' }, + { field: 'metadata.uid', order: 'desc' }, + ], + }); + expect(mockedEndpoint.mock.calls[0][0].url.search).toBe( + '?limit=100&sortField=metadata.name,asc&sortField=metadata.uid,desc&fields=a,b&fullTextFilterTerm=query', + ); + }); + + it('should ignore initial query params if cursor is passed', async () => { + const mockedEndpoint = jest + .fn() + .mockImplementation((_req, res, ctx) => + res(ctx.json({ items: [], totalItems: 0 })), + ); + + server.use(rest.get(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + fields: ['a', 'b'], + limit: 100, + fullTextFilter: { + term: 'query', + }, + orderFields: [{ field: 'metadata.name', order: 'asc' }], + cursor: 'cursor', + }); + expect(mockedEndpoint.mock.calls[0][0].url.search).toBe( + '?cursor=cursor&limit=100&fields=a,b', + ); + }); + + it('should return paginated functions if next and prev cursors are present', async () => { + const mockedEndpoint = jest.fn().mockImplementation((_req, res, ctx) => + res( + ctx.json({ + items: [ + { + apiVersion: 'v1', + kind: 'CustomKind', + metadata: { + namespace: 'default', + name: 'e1', + }, + }, + { + apiVersion: 'v1', + kind: 'CustomKind', + metadata: { + namespace: 'default', + name: 'e2', + }, + }, + ], + pageInfo: { + nextCursor: 'nextcursor', + prevCursor: 'prevcursor', + }, + totalItems: 100, + } as QueryEntitiesResponse), + ), + ); + + server.use(rest.get(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + const response = await client.queryEntities({ + limit: 2, + }); + expect(mockedEndpoint.mock.calls[0][0].url.search).toBe('?limit=2'); + + expect(response?.pageInfo.nextCursor).toBeDefined(); + expect(response?.pageInfo.prevCursor).toBeDefined(); + + await client.queryEntities({ cursor: response!.pageInfo.nextCursor! }); + expect(mockedEndpoint.mock.calls[1][0].url.search).toBe( + '?cursor=nextcursor', + ); + + await client.queryEntities({ cursor: response!.pageInfo.prevCursor! }); + expect(mockedEndpoint.mock.calls[2][0].url.search).toBe( + '?cursor=prevcursor', + ); + }); + }); + describe('getEntityByRef', () => { const existingEntity: Entity = { apiVersion: 'v1', diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index af4262b842..4984d394d4 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -39,9 +39,13 @@ import { ValidateEntityResponse, GetEntitiesByRefsRequest, GetEntitiesByRefsResponse, + QueryEntitiesRequest, + QueryEntitiesResponse, + EntityFilterQuery, } from './types/api'; import { DiscoveryApi } from './types/discovery'; import { FetchApi } from './types/fetch'; +import { isQueryEntitiesInitialRequest } from './utils'; /** * A frontend and backend compatible client for communicating with the Backstage @@ -107,30 +111,7 @@ export class CatalogClient implements CatalogApi { limit, after, } = 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(',')}`); - } - } + const params = this.getParams(filter); if (fields.length) { params.push(`fields=${fields.map(encodeURIComponent).join(',')}`); @@ -223,6 +204,64 @@ export class CatalogClient implements CatalogApi { return { items }; } + /** + * {@inheritdoc CatalogApi.queryEntities} + */ + async queryEntities( + request: QueryEntitiesRequest = {}, + options?: CatalogRequestOptions, + ) { + const params: string[] = []; + + if (isQueryEntitiesInitialRequest(request)) { + const { + fields = [], + filter, + limit, + orderFields, + fullTextFilter, + } = request; + params.push(...this.getParams(filter)); + + if (limit !== undefined) { + params.push(`limit=${limit}`); + } + if (orderFields !== undefined) { + (Array.isArray(orderFields) ? orderFields : [orderFields]).forEach( + ({ field, order }) => params.push(`sortField=${field},${order}`), + ); + } + if (fields.length) { + params.push(`fields=${fields.map(encodeURIComponent).join(',')}`); + } + + const normalizedFullTextFilterTerm = fullTextFilter?.term?.trim(); + if (normalizedFullTextFilterTerm) { + params.push(`fullTextFilterTerm=${normalizedFullTextFilterTerm}`); + } + if (fullTextFilter?.fields?.length) { + params.push(`fullTextFilterFields=${fullTextFilter.fields.join(',')}`); + } + } else { + const { fields = [], limit, cursor } = request; + + params.push(`cursor=${cursor}`); + if (limit !== undefined) { + params.push(`limit=${limit}`); + } + if (fields.length) { + params.push(`fields=${fields.map(encodeURIComponent).join(',')}`); + } + } + + const query = params.length ? `?${params.join('&')}` : ''; + return this.requestRequired<QueryEntitiesResponse>( + 'GET', + `/entities/by-query${query}`, + options, + ); + } + /** * {@inheritdoc CatalogApi.getEntityByRef} */ @@ -290,30 +329,7 @@ export class CatalogClient implements CatalogApi { options?: CatalogRequestOptions, ): Promise<GetEntityFacetsResponse> { 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(',')}`); - } - } + const params = this.getParams(filter); for (const facet of facets) { params.push(`facet=${encodeURIComponent(facet)}`); @@ -466,11 +482,11 @@ export class CatalogClient implements CatalogApi { } } - private async requestRequired( + private async requestRequired<T = any>( method: string, path: string, options?: CatalogRequestOptions, - ): Promise<any> { + ): Promise<T> { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; const headers: Record<string, string> = options?.token ? { Authorization: `Bearer ${options.token}` } @@ -481,7 +497,7 @@ export class CatalogClient implements CatalogApi { throw await ResponseError.fromResponse(response); } - return await response.json(); + return response.json(); } private async requestOptional( @@ -504,4 +520,31 @@ export class CatalogClient implements CatalogApi { return await response.json(); } + + private getParams(filter: EntityFilterQuery = []) { + 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(',')}`); + } + } + return params; + } } diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index d662a9d106..ee6579efca 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -287,10 +287,7 @@ export interface GetEntityFacetsRequest { * (exported from this package), which means that you assert on the existence * of that key, no matter what its value is. */ - filter?: - | Record<string, string | symbol | (string | symbol)[]>[] - | Record<string, string | symbol | (string | symbol)[]> - | undefined; + filter?: EntityFilterQuery; /** * Dot separated paths for the facets to extract from each entity. * @@ -380,6 +377,67 @@ export type ValidateEntityResponse = | { valid: true } | { valid: false; errors: SerializedError[] }; +/** + * The request type for {@link CatalogClient.queryEntities}. + * + * @public + */ +export type QueryEntitiesRequest = + | QueryEntitiesInitialRequest + | QueryEntitiesCursorRequest; + +/** + * A request type for {@link CatalogClient.queryEntities}. + * The method takes this type in an initial pagination request, + * when requesting the first batch of entities. + * + * The properties filter, sortField, query and sortFieldOrder, are going + * to be immutable for the entire lifecycle of the following requests. + * + * @public + */ +export type QueryEntitiesInitialRequest = { + fields?: string[]; + limit?: number; + filter?: EntityFilterQuery; + orderFields?: EntityOrderQuery; + fullTextFilter?: { + term: string; + fields?: string[]; + }; +}; + +/** + * A request type for {@link CatalogClient.queryEntities}. + * The method takes this type in a pagination request, following + * the initial request. + * + * @public + */ +export type QueryEntitiesCursorRequest = { + fields?: string[]; + limit?: number; + cursor: string; +}; + +/** + * The response type for {@link CatalogClient.queryEntities}. + * + * @public + */ +export type QueryEntitiesResponse = { + /* The list of entities for the current request */ + items: Entity[]; + /* The number of entities among all the requests */ + totalItems: number; + pageInfo: { + /* The cursor for the next batch of entities */ + nextCursor?: string; + /* The cursor for the previous batch of entities */ + prevCursor?: string; + }; +}; + /** * A client for interacting with the Backstage software catalog through its API. * @@ -414,6 +472,49 @@ export interface CatalogApi { options?: CatalogRequestOptions, ): Promise<GetEntitiesByRefsResponse>; + /** + * Gets paginated entities from the catalog. + * + * @remarks + * + * @example + * + * ``` + * const response = await catalogClient.queryEntities({ + * filter: [{ kind: 'group' }], + * limit: 20, + * fullTextFilter: { + * term: 'A', + * } + * orderFields: { field: 'metadata.name' order: 'asc' }, + * }); + * ``` + * + * this will match all entities of type group having a name starting + * with 'A', ordered by name ascending. + * + * The response will contain a maximum of 20 entities. In case + * more than 20 entities exist, the response will contain a nextCursor + * property that can be used to fetch the next batch of entities. + * + * ``` + * const secondBatchResponse = await catalogClient + * .queryEntities({ cursor: response.nextCursor }); + * ``` + * + * secondBatchResponse will contain the next batch of (maximum) 20 entities, + * together with a prevCursor property, useful to fetch the previous batch. + * + * @public + * + * @param request - Request parameters + * @param options - Additional options + */ + queryEntities( + request?: QueryEntitiesRequest, + options?: CatalogRequestOptions, + ): Promise<QueryEntitiesResponse>; + /** * Gets entity ancestor information, i.e. the hierarchy of parent entities * whose processing resulted in a given entity appearing in the catalog. diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 81b985eb42..9bf35886c8 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -33,5 +33,9 @@ export type { GetEntityFacetsResponse, Location, ValidateEntityResponse, + QueryEntitiesCursorRequest, + QueryEntitiesInitialRequest, + QueryEntitiesRequest, + QueryEntitiesResponse, } from './api'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; diff --git a/plugins/catalog-backend/src/search/util.ts b/packages/catalog-client/src/utils.ts similarity index 56% rename from plugins/catalog-backend/src/search/util.ts rename to packages/catalog-client/src/utils.ts index fd54d397c9..de21cea0d4 100644 --- a/plugins/catalog-backend/src/search/util.ts +++ b/packages/catalog-client/src/utils.ts @@ -14,16 +14,20 @@ * limitations under the License. */ -import { Entity, isUserEntity, isGroupEntity } from '@backstage/catalog-model'; +import { + QueryEntitiesCursorRequest, + QueryEntitiesInitialRequest, + QueryEntitiesRequest, +} from './types/api'; -export function getDocumentText(entity: Entity): string { - const documentTexts: string[] = []; - documentTexts.push(entity.metadata.description || ''); - - if (isUserEntity(entity) || isGroupEntity(entity)) { - if (entity.spec?.profile?.displayName) { - documentTexts.push(entity.spec.profile.displayName); - } - } - return documentTexts.join(' : '); +export function isQueryEntitiesInitialRequest( + request: QueryEntitiesInitialRequest, +): request is QueryEntitiesInitialRequest { + return !(request as QueryEntitiesCursorRequest).cursor; +} + +export function isQueryEntitiesCursorRequest( + request: QueryEntitiesRequest, +): request is QueryEntitiesCursorRequest { + return !isQueryEntitiesInitialRequest(request); } diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index a54ae67997..8d6659c20b 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/catalog-model +## 1.2.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 1.2.0 + +### Minor Changes + +- d51668882d: Adds the tRPC API type to the catalog. + +### Patch Changes + +- aece6c57d2: Add additional validation for location references. +- 2518ef5b8a: Add deprecation tag to kubernetes constants in catalog, constants are now defined in plugin-kubernetes-common +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 1.2.0-next.1 + +### Minor Changes + +- d51668882d: Adds the tRPC API type to the catalog. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 1.1.6-next.0 + +### Patch Changes + +- 2518ef5b8a: Add deprecation tag to kubernetes constants in catalog, constants are now defined in plugin-kubernetes-common + ## 1.1.5 ### Patch Changes diff --git a/packages/catalog-model/alpha-api-report.md b/packages/catalog-model/alpha-api-report.md new file mode 100644 index 0000000000..5b962a7403 --- /dev/null +++ b/packages/catalog-model/alpha-api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/catalog-model" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Entity } from '@backstage/catalog-model'; +import { SerializedError } from '@backstage/errors'; + +// @alpha +export interface AlphaEntity extends Entity { + status?: EntityStatus; +} + +// @alpha +export type EntityStatus = { + items?: EntityStatusItem[]; +}; + +// @alpha +export type EntityStatusItem = { + type: string; + level: EntityStatusLevel; + message: string; + error?: SerializedError; +}; + +// @alpha +export type EntityStatusLevel = 'info' | 'warning' | 'error'; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index b7456bb2f8..b8eb20c590 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -4,24 +4,18 @@ ```ts import { JsonObject } from '@backstage/types'; -import { SerializedError } from '@backstage/errors'; - -// @alpha -export interface AlphaEntity extends Entity { - status?: EntityStatus; -} // @public export const ANNOTATION_EDIT_URL = 'backstage.io/edit-url'; -// @public +// @public @deprecated export const ANNOTATION_KUBERNETES_API_SERVER = 'kubernetes.io/api-server'; -// @public +// @public @deprecated export const ANNOTATION_KUBERNETES_API_SERVER_CA = 'kubernetes.io/api-server-certificate-authority'; -// @public +// @public @deprecated export const ANNOTATION_KUBERNETES_AUTH_PROVIDER = 'kubernetes.io/auth-provider'; @@ -209,22 +203,6 @@ export function entitySchemaValidator<T extends Entity = Entity>( schema?: unknown, ): (data: unknown) => T; -// @alpha -export type EntityStatus = { - items?: EntityStatusItem[]; -}; - -// @alpha -export type EntityStatusItem = { - type: string; - level: EntityStatusLevel; - message: string; - error?: SerializedError; -}; - -// @alpha -export type EntityStatusLevel = 'info' | 'warning' | 'error'; - // @public export class FieldFormatEntityPolicy implements EntityPolicy { constructor(validators?: Validators); diff --git a/packages/catalog-model/examples/all-apis.yaml b/packages/catalog-model/examples/all-apis.yaml index e23f1b3656..472897bfac 100644 --- a/packages/catalog-model/examples/all-apis.yaml +++ b/packages/catalog-model/examples/all-apis.yaml @@ -6,6 +6,7 @@ metadata: spec: targets: - ./apis/hello-world-api.yaml + - ./apis/hello-world-trpc-api.yaml - ./apis/petstore-api.yaml - ./apis/spotify-api.yaml - ./apis/streetlights-api.yaml diff --git a/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml b/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml new file mode 100644 index 0000000000..f5807537b3 --- /dev/null +++ b/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml @@ -0,0 +1,25 @@ +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: hello-world-trpc + description: Hello World example for tRPC +spec: + type: trpc + lifecycle: experimental + owner: team-c + definition: | + import { z } from 'zod'; + import { publicProcedure, router } from '../trpc'; + + export const apiRouter = router({ + version: publicProcedure.query(() => { + return { version: '0.42.0' }; + }), + hello: publicProcedure + .input(z.object({ username: z.string().nullish() }).nullish()) + .query(({ input, ctx }) => { + return { + text: `hello ${input?.username ?? ctx.user?.name ?? 'world'}`, + }; + }), + }); diff --git a/packages/catalog-model/examples/components/artist-lookup-component.yaml b/packages/catalog-model/examples/components/artist-lookup-component.yaml index c0022762b4..a3a3780c84 100644 --- a/packages/catalog-model/examples/components/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/components/artist-lookup-component.yaml @@ -28,6 +28,8 @@ metadata: - url: https://example.com/alert title: Alerts icon: alert + annotations: + backstage.io/linguist: 'https://github.com/backstage/backstage/tree/master/plugins/playlist' spec: type: service lifecycle: experimental diff --git a/packages/catalog-model/examples/components/petstore-component.yaml b/packages/catalog-model/examples/components/petstore-component.yaml index 878fabd551..cdac4ce1ae 100644 --- a/packages/catalog-model/examples/components/petstore-component.yaml +++ b/packages/catalog-model/examples/components/petstore-component.yaml @@ -2,8 +2,11 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: petstore - # This is an extra long description - description: The Petstore is an example API used to show features of the OpenAPI spec. + # This is an extra long description with Markdown + description: | + [The Petstore](http://petstore.example.com) is an example API used to show features of the OpenAPI spec. + - First item + - Second item links: - url: https://github.com/swagger-api/swagger-petstore title: GitHub Repo diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 26d340a90d..c75984643b 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,16 +1,27 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "1.1.5", + "version": "1.2.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "common-library" @@ -25,7 +36,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -48,7 +59,6 @@ "yaml": "^2.0.0" }, "files": [ - "dist", - "alpha" + "dist" ] } diff --git a/packages/catalog-model/src/alpha.ts b/packages/catalog-model/src/alpha.ts new file mode 100644 index 0000000000..1d29c97464 --- /dev/null +++ b/packages/catalog-model/src/alpha.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { + EntityStatus, + EntityStatusItem, + EntityStatusLevel, +} from './entity/EntityStatus'; +export type { AlphaEntity } from './entity/AlphaEntity'; diff --git a/packages/catalog-model/src/entity/AlphaEntity.ts b/packages/catalog-model/src/entity/AlphaEntity.ts new file mode 100644 index 0000000000..6f5c9d4fa7 --- /dev/null +++ b/packages/catalog-model/src/entity/AlphaEntity.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ + +// TODO(Rugvip): Figure out best way to allow this import +// eslint-disable-next-line import/no-extraneous-dependencies +import { Entity } from '@backstage/catalog-model'; +import { EntityStatus } from './EntityStatus'; + +/** + * A version of the `Entity` type that contains unstable alpha fields. + * + * @remarks + * + * Available via the `@backstage/catalog-model/alpha` import. + * + * @alpha + */ +export interface AlphaEntity extends Entity { + /** + * The current status of the entity, as claimed by various sources. + * + * The keys are implementation defined and the values can be any JSON object + * with semantics that match that implementation. + */ + status?: EntityStatus; +} diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 80c9adaffb..8d36fe0134 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -15,7 +15,6 @@ */ import { JsonObject } from '@backstage/types'; -import { EntityStatus } from './EntityStatus'; /** * The parts of the format that's common to all versions/kinds of entity. @@ -54,25 +53,6 @@ export type Entity = { relations?: EntityRelation[]; }; -/** - * A version of the {@link Entity} type that contains unstable alpha fields. - * - * @remarks - * - * Available via the `@backstage/catalog-model/alpha` import. - * - * @alpha - */ -export interface AlphaEntity extends Entity { - /** - * The current status of the entity, as claimed by various sources. - * - * The keys are implementation defined and the values can be any JSON object - * with semantics that match that implementation. - */ - status?: EntityStatus; -} - /** * Metadata fields common to all versions/kinds of entity. * diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts index ac9e813394..729cb87aee 100644 --- a/packages/catalog-model/src/entity/constants.ts +++ b/packages/catalog-model/src/entity/constants.ts @@ -38,6 +38,7 @@ export const ANNOTATION_EDIT_URL = 'backstage.io/edit-url'; /** * Annotation for specifying the API server of a Kubernetes cluster * + * @deprecated Import this constant from `@backstage/plugin-kubernetes-common` instead * @public */ export const ANNOTATION_KUBERNETES_API_SERVER = 'kubernetes.io/api-server'; @@ -45,6 +46,7 @@ export const ANNOTATION_KUBERNETES_API_SERVER = 'kubernetes.io/api-server'; /** * Annotation for specifying the Certificate Authority of an API server for a Kubernetes cluster * + * @deprecated Import this constant from `@backstage/plugin-kubernetes-common` instead * @public */ export const ANNOTATION_KUBERNETES_API_SERVER_CA = @@ -53,6 +55,7 @@ export const ANNOTATION_KUBERNETES_API_SERVER_CA = /** * Annotation for specifying the auth provider for a Kubernetes cluster * + * @deprecated Import this constant from `@backstage/plugin-kubernetes-common` instead * @public */ export const ANNOTATION_KUBERNETES_AUTH_PROVIDER = diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index a97476ba11..269d56ec86 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -15,27 +15,9 @@ */ export * from './conditions'; -export { - DEFAULT_NAMESPACE, - ANNOTATION_EDIT_URL, - ANNOTATION_VIEW_URL, - ANNOTATION_KUBERNETES_API_SERVER, - ANNOTATION_KUBERNETES_API_SERVER_CA, - ANNOTATION_KUBERNETES_AUTH_PROVIDER, -} from './constants'; -export type { - AlphaEntity, - Entity, - EntityLink, - EntityMeta, - EntityRelation, -} from './Entity'; +export * from './constants'; +export type { Entity, EntityLink, EntityMeta, EntityRelation } from './Entity'; export type { EntityEnvelope } from './EntityEnvelope'; -export type { - EntityStatus, - EntityStatusItem, - EntityStatusLevel, -} from './EntityStatus'; export * from './policies'; export { getCompoundEntityRef, diff --git a/packages/catalog-model/src/location/helpers.test.ts b/packages/catalog-model/src/location/helpers.test.ts index 91a2137611..28267dcf37 100644 --- a/packages/catalog-model/src/location/helpers.test.ts +++ b/packages/catalog-model/src/location/helpers.test.ts @@ -50,6 +50,9 @@ describe('parseLocationRef', () => { expect(() => parseLocationRef('https://bleh')).toThrow( "Invalid location ref 'https://bleh', please prefix it with 'url:', e.g. 'url:https://bleh'", ); + expect(() => parseLocationRef('url:javascript:alert()')).toThrow( + "Invalid location ref 'url:javascript:alert()', target is a javascript: URL", + ); }); }); @@ -70,6 +73,12 @@ describe('stringifyLocationRef', () => { expect(() => stringifyLocationRef({ type: 'hello', target: '' })).toThrow( 'Unable to stringify location ref, empty target', ); + expect(() => + // eslint-disable-next-line no-script-url + stringifyLocationRef({ type: 'url', target: 'javascript:alert()' }), + ).toThrow( + "Invalid location ref 'url:javascript:alert()', target is a javascript: URL", + ); }); }); diff --git a/packages/catalog-model/src/location/helpers.ts b/packages/catalog-model/src/location/helpers.ts index 6f9800c5a2..a2f4d84f6f 100644 --- a/packages/catalog-model/src/location/helpers.ts +++ b/packages/catalog-model/src/location/helpers.ts @@ -17,6 +17,11 @@ import { Entity, stringifyEntityRef } from '../entity'; import { ANNOTATION_LOCATION, ANNOTATION_SOURCE_LOCATION } from './annotation'; +// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js +const scriptProtocolPattern = + // eslint-disable-next-line no-control-regex + /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; + /** * Parses a string form location reference. * @@ -56,6 +61,12 @@ export function parseLocationRef(ref: string): { ); } + if (scriptProtocolPattern.test(target)) { + throw new TypeError( + `Invalid location ref '${ref}', target is a javascript: URL`, + ); + } + return { type, target }; } @@ -78,6 +89,12 @@ export function stringifyLocationRef(ref: { throw new TypeError(`Unable to stringify location ref, empty target`); } + if (scriptProtocolPattern.test(target)) { + throw new TypeError( + `Invalid location ref '${type}:${target}', target is a javascript: URL`, + ); + } + return `${type}:${target}`; } diff --git a/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json index fae1225764..3d5af710e0 100644 --- a/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json @@ -46,7 +46,7 @@ "type": { "type": "string", "description": "The type of the API definition.", - "examples": ["openapi", "asyncapi", "graphql", "grpc"], + "examples": ["openapi", "asyncapi", "graphql", "grpc", "trpc"], "minLength": 1 }, "lifecycle": { diff --git a/packages/cli-common/README.md b/packages/cli-common/README.md index 91b774145e..e16398e81a 100644 --- a/packages/cli-common/README.md +++ b/packages/cli-common/README.md @@ -9,4 +9,4 @@ Do not install this package directly, it is an internal package used by [@backst ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 25a0704f56..c42f76c774 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/cli +## 0.22.4-next.0 + +### Patch Changes + +- b4cd145b57: Added a new `migrate package-exports` command that synchronizes package exports fields in all `package.json`s. +- 8bf24946c6: Adjust express dependencies to be the same as the rest of the project +- 17271841de: Updated frontend plugin template to use some more recent features +- Updated dependencies + - @backstage/eslint-plugin@0.1.2-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + +## 0.22.2 + +### Patch Changes + +- 561df21ea3: The `backstage-cli repo test` command now sets a default Jest `--workerIdleMemoryLimit` of 1GB. If needed to ensure that tests are not run in band, `--maxWorkers=2` is set as well. This is the recommended workaround for dealing with Jest workers leaking memory and eventually hitting the heap limit. +- 2815981057: Show module name causing error during build +- dd8a9afe66: Replaced several monorepo lint rules with new rules from `@backstage/eslint-plugin`. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for a full list of rules. +- a9c50af76b: Fixed Webpack loader patterns so that they properly match the file suffix only. +- 66cf22fdc4: Updated dependency `esbuild` to `^0.17.0`. +- 6d3abfded1: Switch to inline source maps for test transpilation, simplifying editor setups. +- 90616df9a8: Added an experimental mode for the `package start` command for backend packages. Enabled by setting `EXPERIMENTAL_BACKEND_START`. +- Updated dependencies + - @backstage/eslint-plugin@0.1.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + +## 0.22.2-next.1 + +### Patch Changes + +- dd8a9afe66: Replaced several monorepo lint rules with new rules from `@backstage/eslint-plugin`. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for a full list of rules. +- 90616df9a8: Added an experimental mode for the `package start` command for backend packages. Enabled by setting `EXPERIMENTAL_BACKEND_START`. +- Updated dependencies + - @backstage/eslint-plugin@0.1.0-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + +## 0.22.2-next.0 + +### Patch Changes + +- 561df21ea3: The `backstage-cli repo test` command now sets a default Jest `--workerIdleMemoryLimit` of 1GB. If needed to ensure that tests are not run in band, `--maxWorkers=2` is set as well. This is the recommended workaround for dealing with Jest workers leaking memory and eventually hitting the heap limit. +- 2815981057: Show module name causing error during build +- 66cf22fdc4: Updated dependency `esbuild` to `^0.17.0`. +- 6d3abfded1: Switch to inline source maps for test transpilation, simplifying editor setups. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + ## 0.22.1 ### Patch Changes diff --git a/packages/cli/README.md b/packages/cli/README.md index 1aa4233032..2b0db93cea 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -25,4 +25,4 @@ To try out the command locally, you can execute the following from the parent di ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli/asset-types/asset-types.d.ts b/packages/cli/asset-types/asset-types.d.ts index 0bd0922004..9bd158bb8e 100644 --- a/packages/cli/asset-types/asset-types.d.ts +++ b/packages/cli/asset-types/asset-types.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /// <reference types="node" /> /// <reference types="react" /> diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index fa9a2aca8d..bad849d283 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -127,11 +127,21 @@ Options: Commands: package-roles package-scripts + package-exports package-lint-configs react-router-deps help [command] ``` +### `backstage-cli migrate package-exports` + +``` +Usage: backstage-cli migrate package-exports [options] + +Options: + -h, --help +``` + ### `backstage-cli migrate package-lint-configs` ``` diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index a40aa79fb3..dde853fdff 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -60,7 +60,7 @@ function createConfig(dir, extraConfig = {}) { '@spotify/eslint-config-typescript', 'prettier', 'plugin:jest/recommended', - 'plugin:monorepo/recommended', + 'plugin:@backstage/recommended', ...(extraExtends ?? []), ], parser: '@typescript-eslint/parser', @@ -88,32 +88,6 @@ function createConfig(dir, extraConfig = {}) { '@typescript-eslint/no-redeclare': 'error', 'no-undef': 'off', 'import/newline-after-import': 'error', - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: dir - ? [ - `!${joinPath(dir, 'src/**')}`, - joinPath(dir, 'src/**/*.test.*'), - joinPath(dir, 'src/**/*.stories.*'), - joinPath(dir, 'src/**/__testUtils__/**'), - joinPath(dir, 'src/**/__mocks__/**'), - joinPath(dir, 'src/setupTests.*'), - ] - : [ - // Legacy config for packages that don't provide a dir - '**/*.test.*', - '**/*.stories.*', - '**/__testUtils__/**', - '**/__mocks__/**', - '**/src/setupTests.*', - '**/dev/**', - ], - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', '@typescript-eslint/consistent-type-assertions': 'error', @@ -135,9 +109,6 @@ function createConfig(dir, extraConfig = {}) { ...(restrictedSrcImports ?? []), ], patterns: [ - // Avoid cross-package imports - '**/../../**/*/src/**', - '**/../../**/*/src', // Prevent imports of stories or tests '*.stories*', '*.test*', @@ -187,8 +158,6 @@ function createConfig(dir, extraConfig = {}) { ...(restrictedImports ?? []), ...(restrictedTestImports ?? []), ], - // Avoid cross-package imports - patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], }, ], }, diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 24385a6b99..0e74b00d5a 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -22,7 +22,6 @@ const { version } = require('../package.json'); const envOptions = { oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS), - enableSourceMaps: Boolean(process.env.ENABLE_SOURCE_MAPS), }; const transformIgnorePattern = [ @@ -130,7 +129,6 @@ async function getProjectConfig(targetPath, extraConfig) { '\\.(mjs|cjs|js)$': [ require.resolve('./jestSwcTransform'), { - sourceMaps: envOptions.enableSourceMaps || !envOptions.oldTests, jsc: { parser: { syntax: 'ecmascript', @@ -141,7 +139,6 @@ async function getProjectConfig(targetPath, extraConfig) { '\\.jsx$': [ require.resolve('./jestSwcTransform'), { - sourceMaps: envOptions.enableSourceMaps || !envOptions.oldTests, jsc: { parser: { syntax: 'ecmascript', @@ -158,7 +155,6 @@ async function getProjectConfig(targetPath, extraConfig) { '\\.ts$': [ require.resolve('./jestSwcTransform'), { - sourceMaps: envOptions.enableSourceMaps || !envOptions.oldTests, jsc: { parser: { syntax: 'typescript', @@ -169,7 +165,6 @@ async function getProjectConfig(targetPath, extraConfig) { '\\.tsx$': [ require.resolve('./jestSwcTransform'), { - sourceMaps: envOptions.enableSourceMaps || !envOptions.oldTests, jsc: { parser: { syntax: 'typescript', diff --git a/packages/cli/package.json b/packages/cli/package.json index 486a64dcdb..0260a1c59b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.22.1", + "version": "0.22.4-next.0", "publishConfig": { "access": "public" }, @@ -34,8 +34,10 @@ "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/eslint-plugin": "workspace:^", "@backstage/release-manifests": "workspace:^", "@backstage/types": "workspace:^", + "@esbuild-kit/cjs-loader": "^2.4.1", "@manypkg/get-packages": "^1.1.3", "@octokit/request": "^6.0.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", @@ -65,9 +67,10 @@ "chalk": "^4.0.0", "chokidar": "^3.3.1", "commander": "^9.1.0", + "cross-spawn": "^7.0.3", "css-loader": "^6.5.1", "diff": "^5.0.0", - "esbuild": "^0.16.0", + "esbuild": "^0.17.0", "esbuild-loader": "^2.18.0", "eslint": "^8.6.0", "eslint-config-prettier": "^8.3.0", @@ -76,7 +79,6 @@ "eslint-plugin-import": "^2.25.4", "eslint-plugin-jest": "^27.0.0", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-monorepo": "^0.3.2", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", "eslint-webpack-plugin": "^3.1.1", @@ -136,6 +138,7 @@ "@backstage/dev-utils": "workspace:^", "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", + "@types/cross-spawn": "^6.0.2", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", @@ -152,9 +155,9 @@ "@types/tar": "^6.1.1", "@types/terser-webpack-plugin": "^5.0.4", "@types/yarnpkg__lockfile": "^1.1.4", - "del": "^6.0.0", + "del": "^7.0.0", "mock-fs": "^5.1.0", - "msw": "^0.49.0", + "msw": "^1.0.0", "nodemon": "^2.0.2", "ts-node": "^10.0.0" }, diff --git a/packages/cli/src/commands/build/buildBackend.ts b/packages/cli/src/commands/build/buildBackend.ts index d7c4f72dbe..7e61f9ae85 100644 --- a/packages/cli/src/commands/build/buildBackend.ts +++ b/packages/cli/src/commands/build/buildBackend.ts @@ -37,6 +37,7 @@ export async function buildBackend(options: BuildBackendOptions) { // We build the target package without generating type declarations. await buildPackage({ targetDir: options.targetDir, + packageJson: pkg, outputs: new Set([Output.cjs]), }); diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 628e46df85..ddec5c6a4a 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -116,7 +116,7 @@ export function registerScriptCommand(program: Command) { ) .option( '--experimental-type-build', - 'Enable experimental type build. Does not apply to app or backend packages.', + 'Enable experimental type build. Does not apply to app or backend packages. [DEPRECATED]', ) .option( '--skip-build-dependencies', @@ -191,6 +191,13 @@ export function registerMigrateCommand(program: Command) { lazy(() => import('./migrate/packageScripts').then(m => m.command)), ); + command + .command('package-exports') + .description('Synchronize package subpath export definitions') + .action( + lazy(() => import('./migrate/packageExports').then(m => m.command)), + ); + command .command('package-lint-configs') .description( diff --git a/packages/cli/src/commands/migrate/packageExports.ts b/packages/cli/src/commands/migrate/packageExports.ts new file mode 100644 index 0000000000..ff01f939f9 --- /dev/null +++ b/packages/cli/src/commands/migrate/packageExports.ts @@ -0,0 +1,121 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { ExtendedPackageJSON, PackageGraph } from '../../lib/monorepo'; + +function trimRelative(path: string): string { + if (path.startsWith('./')) { + return path.slice(2); + } + return path; +} + +export async function command() { + const packages = await PackageGraph.listTargetPackages(); + + await Promise.all( + packages.map(async ({ dir, packageJson }) => { + let changed = false; + let newPackageJson = packageJson; + + let { exports: exp } = newPackageJson; + if (!exp) { + return; + } + if (Array.isArray(exp)) { + throw new Error('Unexpected array in package.json exports field'); + } + + // If exports is a string we rewrite it to an object to add package.json + if (typeof exp === 'string') { + changed = true; + exp = { '.': exp }; + newPackageJson.exports = exp; + } else if (typeof exp !== 'object') { + return; + } + + if (!exp['./package.json']) { + changed = true; + exp['./package.json'] = './package.json'; + } + + const existingTypesVersions = JSON.stringify(packageJson.typesVersions); + + const typeEntries: Record<string, [string]> = {}; + for (const [path, value] of Object.entries(exp)) { + // Main entry point does not need to be listed + if (path === '.') { + continue; + } + const newPath = trimRelative(path); + + if (typeof value === 'string') { + typeEntries[newPath] = [trimRelative(value)]; + } else if ( + value && + typeof value === 'object' && + !Array.isArray(value) + ) { + if (typeof value.types === 'string') { + typeEntries[newPath] = [trimRelative(value.types)]; + } else if (typeof value.default === 'string') { + typeEntries[newPath] = [trimRelative(value.default)]; + } + } + } + + const typesVersions = { '*': typeEntries }; + if (existingTypesVersions !== JSON.stringify(typesVersions)) { + console.log(`Synchronizing exports in ${packageJson.name}`); + const newPkgEntries = Object.entries(newPackageJson).filter( + ([name]) => name !== 'typesVersions', + ); + newPkgEntries.splice( + newPkgEntries.findIndex(([name]) => name === 'exports') + 1, + 0, + ['typesVersions', typesVersions], + ); + + newPackageJson = Object.fromEntries( + newPkgEntries, + ) as ExtendedPackageJSON; + changed = true; + } + + // Remove the legacy fields from publishConfig, which are no longer needed + const publishConfig = newPackageJson.publishConfig as + | Record<string, string> + | undefined; + if (publishConfig) { + for (const field of ['main', 'module', 'browser', 'types']) { + if (publishConfig[field]) { + delete publishConfig[field]; + changed = true; + } + } + } + + if (changed) { + await fs.writeJson(resolvePath(dir, 'package.json'), newPackageJson, { + spaces: 2, + }); + } + }), + ); +} diff --git a/packages/cli/src/commands/pack.ts b/packages/cli/src/commands/pack.ts index 6c88ac055f..9c101e97fe 100644 --- a/packages/cli/src/commands/pack.ts +++ b/packages/cli/src/commands/pack.ts @@ -14,77 +14,16 @@ * limitations under the License. */ -import fs from 'fs-extra'; +import { + productionPack, + revertProductionPack, +} from '../lib/packager/productionPack'; import { paths } from '../lib/paths'; -import { join as joinPath } from 'path'; - -const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes']; - -const PKG_PATH = 'package.json'; -const PKG_BACKUP_PATH = 'package.json-prepack'; - -function resolveEntrypoint(pkg: any, name: string) { - const targetEntry = pkg.publishConfig[name] || pkg[name]; - return targetEntry && joinPath('..', targetEntry); -} - -// Writes e.g. alpha/package.json -async function writeReleaseStageEntrypoint(pkg: any, stage: 'alpha' | 'beta') { - await fs.ensureDir(paths.resolveTarget(stage)); - await fs.writeJson( - paths.resolveTarget(stage, PKG_PATH), - { - name: pkg.name, - version: pkg.version, - main: resolveEntrypoint(pkg, 'main'), - module: resolveEntrypoint(pkg, 'module'), - browser: resolveEntrypoint(pkg, 'browser'), - types: joinPath('..', pkg.publishConfig[`${stage}Types`]), - }, - { encoding: 'utf8', spaces: 2 }, - ); -} export const pre = async () => { - const pkgPath = paths.resolveTarget(PKG_PATH); - - const pkgContent = await fs.readFile(pkgPath, 'utf8'); - const pkg = JSON.parse(pkgContent); - await fs.writeFile(PKG_BACKUP_PATH, pkgContent); - - const publishConfig = pkg.publishConfig ?? {}; - for (const key of Object.keys(publishConfig)) { - if (!SKIPPED_KEYS.includes(key)) { - pkg[key] = publishConfig[key]; - } - } - await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 }); - - if (publishConfig.alphaTypes) { - await writeReleaseStageEntrypoint(pkg, 'alpha'); - } - if (publishConfig.betaTypes) { - await writeReleaseStageEntrypoint(pkg, 'beta'); - } + await productionPack({ packageDir: paths.targetDir }); }; export const post = async () => { - // postpack isn't called by yarn right now, so it needs to be called manually - try { - await fs.move(PKG_BACKUP_PATH, PKG_PATH, { overwrite: true }); - - // Check if we're shipping types for other release stages, clean up in that case - const pkg = await fs.readJson(PKG_PATH); - if (pkg.publishConfig?.alphaTypes) { - await fs.remove(paths.resolveTarget('alpha')); - } - if (pkg.publishConfig?.betaTypes) { - await fs.remove(paths.resolveTarget('beta')); - } - } catch (error) { - console.warn( - `Failed to restore package.json during postpack, ${error}. ` + - 'Your package will be fine but you may have ended up with some garbage in the repo.', - ); - } + await revertProductionPack(paths.targetDir); }; diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index a6bf87da82..d5602a4008 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -130,6 +130,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> { return { targetDir: pkg.dir, + packageJson: pkg.packageJson, outputs, logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, minify: buildOptions.minify, diff --git a/packages/cli/src/commands/repo/test.test.ts b/packages/cli/src/commands/repo/test.test.ts new file mode 100644 index 0000000000..d2f2485717 --- /dev/null +++ b/packages/cli/src/commands/repo/test.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createFlagFinder } from './test'; + +describe('createFlagFinder', () => { + it('finds flags', () => { + const find = createFlagFinder([ + '--foo', + '--no-bar', + '-b', + '-c', + '--baz=1', + '--qux', + '2', + '-de', + ]); + + expect( + find('--foo', '--bar', '-b', '-c', '--baz', '--qux', '-d', '-e'), + ).toBe(true); + expect(find('--foo')).toBe(true); + expect(find('--bar')).toBe(true); + expect(find('--no-bar')).toBe(false); + expect(find('-a')).toBe(false); + expect(find('-b')).toBe(true); + expect(find('-c')).toBe(true); + expect(find('-d')).toBe(true); + expect(find('-e')).toBe(true); + expect(find('--baz')).toBe(true); + expect(find('--qux')).toBe(true); + expect(find('--qux')).toBe(true); + }); +}); diff --git a/packages/cli/src/commands/repo/test.ts b/packages/cli/src/commands/repo/test.ts index 95bfe8f449..2dd778677a 100644 --- a/packages/cli/src/commands/repo/test.ts +++ b/packages/cli/src/commands/repo/test.ts @@ -14,18 +14,36 @@ * limitations under the License. */ +import os from 'os'; import { Command, OptionValues } from 'commander'; import { PackageGraph } from '../../lib/monorepo'; import { paths } from '../../lib/paths'; import { runCheck } from '../../lib/run'; -function includesAnyOf(hayStack: string[], ...needles: string[]) { - for (const needle of needles) { - if (hayStack.includes(needle)) { - return true; +export function createFlagFinder(args: string[]) { + const flags = new Set<string>(); + + for (const arg of args) { + if (arg.startsWith('--no-')) { + flags.add(`--${arg.slice('--no-'.length)}`); + } else if (arg.startsWith('--')) { + flags.add(arg.split('=')[0]); + } else if (arg.startsWith('-')) { + const shortFlags = arg.slice(1).split(''); + for (const shortFlag of shortFlags) { + flags.add(`-${shortFlag}`); + } } } - return false; + + return (...findFlags: string[]) => { + for (const flag of findFlags) { + if (flags.has(flag)) { + return true; + } + } + return false; + }; } function removeOptionArg(args: string[], option: string) { @@ -55,24 +73,19 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> { const allArgs = parent.args as string[]; const args = allArgs.slice(allArgs.indexOf('test') + 1); + const hasFlags = createFlagFinder(args); + // Only include our config if caller isn't passing their own config - if (!includesAnyOf(args, '-c', '--config')) { + if (!hasFlags('-c', '--config')) { args.push('--config', paths.resolveOwn('config/jest.js')); } - if (!includesAnyOf(args, '--no-passWithNoTests', '--passWithNoTests=false')) { + if (!hasFlags('--passWithNoTests')) { args.push('--passWithNoTests'); } // Run in watch mode unless in CI, coverage mode, or running all tests - if ( - !process.env.CI && - !args.includes('--coverage') && - // explicitly no watching - !includesAnyOf(args, '--no-watch', '--watch=false', '--watchAll=false') && - // already watching - !includesAnyOf(args, '--watch', '--watchAll') - ) { + if (!process.env.CI && !hasFlags('--coverage', '--watch', '--watchAll')) { const isGitRepo = () => runCheck('git', 'rev-parse', '--is-inside-work-tree'); const isMercurialRepo = () => runCheck('hg', '--cwd', '.', 'root'); @@ -84,11 +97,30 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> { } } + // When running tests from the repo root in large repos you can easily hit the heap limit. + // This is because Jest workers leak a lot of memory, and the workaround is to limit worker memory. + // We set a default memory limit, but if an explicit one is supplied it will be used instead + if (!hasFlags('--workerIdleMemoryLimit')) { + args.push('--workerIdleMemoryLimit=1000M'); + } + + // In order for the above worker memory limit to work we need to make sure the worker + // count is set to at least 2, as the tests will otherwise run in-band. + // Depending on the mode tests are run with the default count is either cpus-1, or cpus/2. + // This means that if we've got at 4 or more cores we'll always get at least 2 workers, but + // otherwise we need to set the worker count explicitly unless already done. + if ( + os.cpus().length <= 3 && + !hasFlags('-i', '--runInBand', '-w', '--maxWorkers') + ) { + args.push('--maxWorkers=2'); + } + if (opts.since) { removeOptionArg(args, '--since'); } - if (opts.since && !args.some(arg => arg.startsWith('--selectProjects'))) { + if (opts.since && !hasFlags('--selectProjects')) { const packages = await PackageGraph.listTargetPackages(); const graph = PackageGraph.fromPackages(packages); const changedPackages = await graph.listChangedPackages({ diff --git a/packages/cli/src/commands/start/startBackend.ts b/packages/cli/src/commands/start/startBackend.ts index 61ada7a291..3e6a13be96 100644 --- a/packages/cli/src/commands/start/startBackend.ts +++ b/packages/cli/src/commands/start/startBackend.ts @@ -17,6 +17,7 @@ import fs from 'fs-extra'; import { paths } from '../../lib/paths'; import { serveBackend } from '../../lib/bundler'; +import { startBackendExperimental } from '../../lib/experimental/startBackendExperimental'; interface StartBackendOptions { checksEnabled: boolean; @@ -25,17 +26,28 @@ interface StartBackendOptions { } export async function startBackend(options: StartBackendOptions) { - // Cleaning dist/ before we start the dev process helps work around an issue - // where we end up with the entrypoint executing multiple times, causing - // a port bind conflict among other things. - await fs.remove(paths.resolveTarget('dist')); + if (process.env.EXPERIMENTAL_BACKEND_START) { + const waitForExit = await startBackendExperimental({ + entry: 'src/index', + checksEnabled: false, // not supported + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); - const waitForExit = await serveBackend({ - entry: 'src/index', - checksEnabled: options.checksEnabled, - inspectEnabled: options.inspectEnabled, - inspectBrkEnabled: options.inspectBrkEnabled, - }); + await waitForExit(); + } else { + // Cleaning dist/ before we start the dev process helps work around an issue + // where we end up with the entrypoint executing multiple times, causing + // a port bind conflict among other things. + await fs.remove(paths.resolveTarget('dist')); - await waitForExit(); + const waitForExit = await serveBackend({ + entry: 'src/index', + checksEnabled: options.checksEnabled, + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); + + await waitForExit(); + } } diff --git a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts index 2533131a40..cf37c2a664 100644 --- a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts +++ b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts @@ -50,7 +50,7 @@ export async function buildTypeDefinitionsWorker( */ const { PackageJsonLookup, - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-undeclared-imports } = require('@rushstack/node-core-library/lib/PackageJsonLookup'); const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor; diff --git a/packages/cli/src/lib/builder/config.test.ts b/packages/cli/src/lib/builder/config.test.ts index a2e99da73b..733c35fb55 100644 --- a/packages/cli/src/lib/builder/config.test.ts +++ b/packages/cli/src/lib/builder/config.test.ts @@ -24,6 +24,11 @@ describe('makeRollupConfigs', () => { const [config] = await makeRollupConfigs({ outputs: new Set([Output.cjs]), + packageJson: { + name: 'test', + version: '0.0.0', + main: './src/index.ts', + }, }); const external = config.external as Exclude< ExternalOption, diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 1cfaf15bfc..ecd755d05b 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -31,6 +31,10 @@ import { forwardFileImports } from './plugins'; import { BuildOptions, Output } from './types'; import { paths } from '../paths'; import { svgrTemplate } from '../svgrTemplate'; +import { ExtendedPackageJSON } from '../monorepo'; +import { readEntryPoints } from '../monorepo/entryPoints'; + +const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx']; function isFileImport(source: string) { if (source.startsWith('.')) { @@ -50,6 +54,13 @@ export async function makeRollupConfigs( ): Promise<RollupOptions[]> { const configs = new Array<RollupOptions>(); const targetDir = options.targetDir ?? paths.targetDir; + + let targetPkg = options.packageJson; + if (!targetPkg) { + const packagePath = resolvePath(targetDir, 'package.json'); + targetPkg = (await fs.readJson(packagePath)) as ExtendedPackageJSON; + } + const onwarn = ({ code, message }: RollupWarning) => { if (code === 'EMPTY_BUNDLE') { return; // We don't care about this one @@ -62,111 +73,118 @@ export async function makeRollupConfigs( }; const distDir = resolvePath(targetDir, 'dist'); + const entryPoints = readEntryPoints(targetPkg); - if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) { - const output = new Array<OutputOptions>(); - const mainFields = ['module', 'main']; - - if (options.outputs.has(Output.cjs)) { - output.push({ - dir: distDir, - entryFileNames: 'index.cjs.js', - chunkFileNames: 'cjs/[name]-[hash].cjs.js', - format: 'commonjs', - sourcemap: true, - }); - } - if (options.outputs.has(Output.esm)) { - output.push({ - dir: distDir, - entryFileNames: 'index.esm.js', - chunkFileNames: 'esm/[name]-[hash].esm.js', - format: 'module', - sourcemap: true, - }); - // Assume we're building for the browser if ESM output is included - mainFields.unshift('browser'); + for (const { path, name, ext } of entryPoints) { + if (!SCRIPT_EXTS.includes(ext)) { + continue; } - configs.push({ - input: resolvePath(targetDir, 'src/index.ts'), - output, - onwarn, - preserveEntrySignatures: 'strict', - // All module imports are always marked as external - external: (source, importer, isResolved) => - Boolean(importer && !isResolved && !isFileImport(source)), - plugins: [ - resolve({ mainFields }), - commonjs({ - include: /node_modules/, - exclude: [/\/[^/]+\.(?:stories|test)\.[^/]+$/], - }), - postcss(), - forwardFileImports({ - exclude: /\.icon\.svg$/, - include: [ - /\.svg$/, - /\.png$/, - /\.gif$/, - /\.jpg$/, - /\.jpeg$/, - /\.eot$/, - /\.woff$/, - /\.woff2$/, - /\.ttf$/, - ], - }), - json(), - yaml(), - svgr({ - include: /\.icon\.svg$/, - template: svgrTemplate, - }), - esbuild({ - target: 'es2019', - minify: options.minify, - }), - ], - }); - } + if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) { + const output = new Array<OutputOptions>(); + const mainFields = ['module', 'main']; - if (options.outputs.has(Output.types) && !options.useApiExtractor) { - const typesInput = paths.resolveTargetRoot( - 'dist-types', - relativePath(paths.targetRoot, targetDir), - 'src/index.d.ts', - ); + if (options.outputs.has(Output.cjs)) { + output.push({ + dir: distDir, + entryFileNames: `${name}.cjs.js`, + chunkFileNames: `cjs/${name}/[name]-[hash].cjs.js`, + format: 'commonjs', + sourcemap: true, + }); + } + if (options.outputs.has(Output.esm)) { + output.push({ + dir: distDir, + entryFileNames: `${name}.esm.js`, + chunkFileNames: `esm/${name}/[name]-[hash].esm.js`, + format: 'module', + sourcemap: true, + }); + // Assume we're building for the browser if ESM output is included + mainFields.unshift('browser'); + } - const declarationsExist = await fs.pathExists(typesInput); - if (!declarationsExist) { - const path = relativePath(targetDir, typesInput); - throw new Error( - `No declaration files found at ${path}, be sure to run ${chalk.bgRed.white( - 'yarn tsc', - )} to generate .d.ts files before packaging`, + configs.push({ + input: resolvePath(targetDir, path), + output, + onwarn, + preserveEntrySignatures: 'strict', + // All module imports are always marked as external + external: (source, importer, isResolved) => + Boolean(importer && !isResolved && !isFileImport(source)), + plugins: [ + resolve({ mainFields }), + commonjs({ + include: /node_modules/, + exclude: [/\/[^/]+\.(?:stories|test)\.[^/]+$/], + }), + postcss(), + forwardFileImports({ + exclude: /\.icon\.svg$/, + include: [ + /\.svg$/, + /\.png$/, + /\.gif$/, + /\.jpg$/, + /\.jpeg$/, + /\.eot$/, + /\.woff$/, + /\.woff2$/, + /\.ttf$/, + ], + }), + json(), + yaml(), + svgr({ + include: /\.icon\.svg$/, + template: svgrTemplate, + }), + esbuild({ + target: 'es2019', + minify: options.minify, + }), + ], + }); + } + + if (options.outputs.has(Output.types) && !options.useApiExtractor) { + const typesInput = paths.resolveTargetRoot( + 'dist-types', + relativePath(paths.targetRoot, targetDir), + path.replace(/\.ts$/, '.d.ts'), ); - } - configs.push({ - input: typesInput, - output: { - file: resolvePath(distDir, 'index.d.ts'), - format: 'es', - }, - external: [ - /\.css$/, - /\.scss$/, - /\.sass$/, - /\.svg$/, - /\.eot$/, - /\.woff$/, - /\.woff2$/, - /\.ttf$/, - ], - onwarn, - plugins: [dts()], - }); + const declarationsExist = await fs.pathExists(typesInput); + if (!declarationsExist) { + const declarationPath = relativePath(targetDir, typesInput); + throw new Error( + `No declaration files found at ${declarationPath}, be sure to run ${chalk.bgRed.white( + 'yarn tsc', + )} to generate .d.ts files before packaging`, + ); + } + + configs.push({ + input: typesInput, + output: { + file: resolvePath(distDir, `${name}.d.ts`), + format: 'es', + }, + external: [ + /\.css$/, + /\.scss$/, + /\.sass$/, + /\.svg$/, + /\.eot$/, + /\.woff$/, + /\.woff2$/, + /\.ttf$/, + ], + onwarn, + plugins: [dts()], + }); + } } return configs; diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index f43d84cd92..de7e9b10eb 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { ExtendedPackageJSON } from '../monorepo'; + export enum Output { esm, cjs, @@ -23,6 +25,7 @@ export enum Output { export type BuildOptions = { logPrefix?: string; targetDir?: string; + packageJson?: ExtendedPackageJSON; outputs: Set<Output>; minify?: boolean; useApiExtractor?: boolean; diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index a8967bb250..b26c2f9f0d 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -32,6 +32,10 @@ import chalk from 'chalk'; const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; +function applyContextToError(error: string, moduleName: string): string { + return `Failed to compile '${moduleName}':\n ${error}`; +} + export async function buildBundle(options: BuildOptions) { const { statsJsonEnabled, schema: configSchema } = options; @@ -63,10 +67,7 @@ export async function buildBundle(options: BuildOptions) { ); } - const { stats } = await build(config, isCi).catch(error => { - console.log(chalk.red('Failed to compile.\n')); - throw new Error(`Failed to compile.\n${error.message || error}`); - }); + const { stats } = await build(config, isCi); if (!stats) { throw new Error('No stats returned'); @@ -114,7 +115,7 @@ async function build(config: webpack.Configuration, isCi: boolean) { ); if (!stats) { - throw new Error('No stats provided'); + throw new Error('Failed to compile: No stats provided'); } const serializedStats = stats.toJson({ @@ -122,29 +123,33 @@ async function build(config: webpack.Configuration, isCi: boolean) { warnings: true, errors: true, }); - // NOTE(freben): The code below that extracts the message part of the errors, - // is due to react-dev-utils not yet being compatible with webpack 5. This - // may be possible to remove (just passing the serialized stats object - // directly into the format function) after a new release of react-dev-utils - // has been made available. - // See https://github.com/facebook/create-react-app/issues/9880 const { errors, warnings } = formatWebpackMessages({ - errors: serializedStats.errors?.map(e => (e.message ? e.message : e)), - warnings: serializedStats.warnings?.map(e => (e.message ? e.message : e)), + errors: serializedStats.errors, + warnings: serializedStats.warnings, }); if (errors.length) { // Only keep the first error. Others are often indicative // of the same problem, but confuse the reader with noise. - throw new Error(errors[0]); + const errorWithContext = applyContextToError( + errors[0], + serializedStats.errors?.[0]?.moduleName ?? '', + ); + throw new Error(errorWithContext); } if (isCi && warnings.length) { + const warningsWithContext = warnings.map((warning, i) => { + return applyContextToError( + warning, + serializedStats.warnings?.[i]?.moduleName ?? '', + ); + }); console.log( chalk.yellow( '\nTreating warnings as errors because process.env.CI = true.\n', ), ); - throw new Error(warnings.join('\n\n')); + throw new Error(warningsWithContext.join('\n\n')); } return { stats }; diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index ce216f5c59..01f323a082 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -15,7 +15,7 @@ */ import fs from 'fs-extra'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, join as joinPath } from 'path'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; @@ -36,6 +36,8 @@ import { runPlain } from '../run'; import ESLintPlugin from 'eslint-webpack-plugin'; import pickBy from 'lodash/pickBy'; import yn from 'yn'; +import { readEntryPoints } from '../monorepo/entryPoints'; +import { ExtendedPackage } from '../monorepo'; const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE'; @@ -100,7 +102,7 @@ export async function createConfig( }), new ESLintPlugin({ context: paths.targetPath, - files: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'], + files: ['**/*.(ts|tsx|mts|cts|js|jsx|mjs|cjs)'], }), ); } @@ -232,7 +234,10 @@ export async function createBackendConfig( // Find all local monorepo packages and their node_modules, and mark them as external. const { packages } = await getPackages(cliPaths.targetDir); - const localPackageNames = packages.map(p => p.packageJson.name); + const localPackageEntryPoints = packages.flatMap(p => { + const entryPoints = readEntryPoints((p as ExtendedPackage).packageJson); + return entryPoints.map(e => joinPath(p.packageJson.name, e.mount)); + }); const moduleDirs = packages.map(p => resolvePath(p.dir, 'node_modules')); // See frontend config const externalPkgs = packages.filter(p => !isChildPath(paths.root, p.dir)); @@ -261,7 +266,7 @@ export async function createBackendConfig( nodeExternalsWithResolve({ modulesDir: paths.rootNodeModules, additionalModuleDirs: moduleDirs, - allowlist: ['webpack/hot/poll?100', ...localPackageNames], + allowlist: ['webpack/hot/poll?100', ...localPackageEntryPoints], }), ], target: 'node' as const, @@ -325,7 +330,7 @@ export async function createBackendConfig( typescript: { configFile: paths.targetTsConfig }, }), new ESLintPlugin({ - files: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'], + files: ['**/*.(ts|tsx|mts|cts|js|jsx|mjs|cjs)'], }), ] : []), diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 9a93f550dd..e0308f5c2d 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -107,7 +107,7 @@ export const transforms = (options: TransformOptions): Transforms => { ], }, { - test: /\.(js|mjs|cjs)/, + test: /\.(js|mjs|cjs)$/, resolve: { fullySpecified: false, }, @@ -141,9 +141,10 @@ export const transforms = (options: TransformOptions): Transforms => { /\.gif$/, /\.jpe?g$/, /\.png$/, - /\.frag/, - { and: [/\.svg/, { not: [/\.icon\.svg/] }] }, - /\.xml/, + /\.frag$/, + /\.vert$/, + { and: [/\.svg$/, { not: [/\.icon\.svg$/] }] }, + /\.xml$/, ], type: 'asset/resource', generator: { diff --git a/packages/cli/src/lib/experimental/IpcServer.ts b/packages/cli/src/lib/experimental/IpcServer.ts new file mode 100644 index 0000000000..8ceb72b046 --- /dev/null +++ b/packages/cli/src/lib/experimental/IpcServer.ts @@ -0,0 +1,102 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { serializeError } from '@backstage/errors'; +import { ChildProcess } from 'child_process'; + +interface RequestMeta { + generation: number; +} + +type MethodHandler<TRequest, TResponse> = ( + req: TRequest, + meta: RequestMeta, +) => Promise<TResponse>; + +interface Request { + id: number; + method: string; + body: unknown; + type: string; +} + +const requestType = '@backstage/cli/channel/request'; +const responseType = '@backstage/cli/channel/response'; + +export class IpcServer { + #generation = 1; + #methods = new Map<string, MethodHandler<any, any>>(); + + addChild(child: ChildProcess) { + const generation = this.#generation++; + const sendMessage = child.send?.bind(child); + if (!sendMessage) { + return; + } + + const messageListener = (request: Request) => { + if (request.type !== requestType) { + return; + } + + const handler = this.#methods.get(request.method); + if (!handler) { + sendMessage({ + type: responseType, + id: request.id, + error: { + name: 'NotFoundError', + message: `No handler registered for method ${request.method}`, + }, + }); + return; + } + + Promise.resolve() + .then(() => handler(request.body, { generation })) + .then(response => + sendMessage({ + type: responseType, + id: request.id, + body: response, + }), + ) + .catch(error => + sendMessage({ + type: responseType, + id: request.id, + error: serializeError(error), + }), + ); + }; + + child.addListener('message', messageListener as (req: unknown) => void); + + child.addListener('exit', () => { + child.removeListener('message', messageListener); + }); + } + + registerMethod<TRequest, TResponse>( + method: string, + handler: MethodHandler<TRequest, TResponse>, + ) { + if (this.#methods.has(method)) { + throw new Error(`A handler is already registered for method ${method}`); + } + this.#methods.set(method, handler); + } +} diff --git a/packages/cli/src/lib/experimental/ServerDataStore.ts b/packages/cli/src/lib/experimental/ServerDataStore.ts new file mode 100644 index 0000000000..c0232157cb --- /dev/null +++ b/packages/cli/src/lib/experimental/ServerDataStore.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IpcServer } from './IpcServer'; + +interface StorageItem { + generation: number; + data: unknown; +} + +interface SaveRequest { + key: string; + data: unknown; +} + +interface SaveResponse { + saved: boolean; +} + +interface LoadRequest { + key: string; +} + +interface LoadResponse { + loaded: boolean; + data: unknown; +} + +export class ServerDataStore { + static bind(server: IpcServer): void { + const store = new Map<string, StorageItem>(); + + server.registerMethod<SaveRequest, SaveResponse>( + 'DevDataStore.save', + async (request, { generation }) => { + const { key, data } = request; + if (!key) { + throw new Error('Key is required in DevDataStore.save'); + } + + const item = store.get(key); + + if (!item) { + store.set(key, { generation, data }); + return { saved: true }; + } + + if (item.generation > generation) { + return { saved: false }; + } + + store.set(key, { generation, data }); + return { saved: true }; + }, + ); + + server.registerMethod<LoadRequest, LoadResponse>( + 'DevDataStore.load', + async request => { + const item = store.get(request.key); + return { loaded: Boolean(item), data: item?.data }; + }, + ); + } +} diff --git a/packages/cli/src/lib/experimental/startBackendExperimental.ts b/packages/cli/src/lib/experimental/startBackendExperimental.ts new file mode 100644 index 0000000000..c08fbda0da --- /dev/null +++ b/packages/cli/src/lib/experimental/startBackendExperimental.ts @@ -0,0 +1,138 @@ +/* + * 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 { BackendServeOptions } from '../bundler/types'; +import type { ChildProcess } from 'child_process'; +import { fileURLToPath } from 'url'; +import { isAbsolute as isAbsolutePath } from 'path'; +import { FSWatcher, watch } from 'chokidar'; +import { IpcServer } from './IpcServer'; +import { ServerDataStore } from './ServerDataStore'; +import debounce from 'lodash/debounce'; +import spawn from 'cross-spawn'; +import { paths } from '../paths'; + +const loaderArgs = ['--require', require.resolve('@esbuild-kit/cjs-loader')]; + +export async function startBackendExperimental(options: BackendServeOptions) { + const envEnv = process.env as { NODE_ENV: string }; + if (!envEnv.NODE_ENV) { + envEnv.NODE_ENV = 'development'; + } + + // Set up the parent IPC server and bind the available services + const server = new IpcServer(); + ServerDataStore.bind(server); + + let exiting = false; + let child: ChildProcess | undefined; + let watcher: FSWatcher | undefined = undefined; + let shutdownPromise: Promise<void> | undefined = undefined; + + const restart = debounce(async () => { + // If a re-trigger happens during an existing shutdown, we just ignore it + if (shutdownPromise) { + return; + } + + if (child && !child.killed && child.exitCode === null) { + // We always wait for the existing process to exit, to make sure we don't get IPC conflicts + shutdownPromise = new Promise(resolve => child!.once('exit', resolve)); + child.kill(); + await shutdownPromise; + shutdownPromise = undefined; + } + + // We've received a shutdown signal + if (exiting) { + return; + } + + const optionArgs = new Array<string>(); + if (options.inspectEnabled) { + optionArgs.push('--inspect'); + } else if (options.inspectBrkEnabled) { + optionArgs.push('--inspect-brk'); + } + + const userArgs = process.argv + .slice(['node', 'backstage-cli', 'package', 'start'].length) + .filter(arg => !optionArgs.includes(arg)); + + child = spawn( + process.execPath, + [...loaderArgs, ...optionArgs, options.entry, ...userArgs], + { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + env: { + ...process.env, + BACKSTAGE_CLI_CHANNEL: '1', + ESBK_TSCONFIG_PATH: paths.resolveTargetRoot('tsconfig.json'), + }, + serialization: 'advanced', + }, + ); + + server.addChild(child); + + // This captures messages sent by @esbuild-kit/cjs-loader + child.on('message', (data: { type?: string } | null) => { + if (typeof data === 'object' && data?.type === 'dependency') { + let path = (data as { path: string }).path; + if (path.startsWith('file:')) { + path = fileURLToPath(path); + } + + if (isAbsolutePath(path)) { + watcher?.add(path); + } + } + }); + }, 100); + + restart(); + + watcher = watch([paths.targetDir], { + cwd: process.cwd(), + ignored: ['**/.*/**', '**/node_modules/**'], + ignoreInitial: true, + ignorePermissionErrors: true, + }).on('all', restart); + + // Trigger restart on hitting enter in the terminal + process.stdin.on('data', restart); + + const exitPromise = new Promise<void>(resolveExitPromise => { + async function handleSignal(signal: NodeJS.Signals) { + exiting = true; + + // Forward signals to child and wait for it to exit if still running + if (child && child.exitCode === null) { + await new Promise(resolve => { + child!.on('close', resolve); + child!.kill(signal); + }); + } + + resolveExitPromise(); + } + + process.once('SIGINT', handleSignal); + process.once('SIGTERM', handleSignal); + }); + + return () => exitPromise; +} diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index a1fc95e669..bb203c01a5 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -20,10 +20,15 @@ import { paths } from '../paths'; import { PackageRole } from '../role'; import { listChangedFiles, readFileAtRef } from '../git'; import { Lockfile } from '../versioning'; +import { JsonValue } from '@backstage/types'; type PackageJSON = Package['packageJson']; export interface ExtendedPackageJSON extends PackageJSON { + main?: string; + module?: string; + types?: string; + scripts?: { [key: string]: string; }; @@ -34,6 +39,19 @@ export interface ExtendedPackageJSON extends PackageJSON { backstage?: { role?: PackageRole; }; + + exports?: JsonValue; + typesVersions?: Record<string, Record<string, string[]>>; + + files?: string[]; + + publishConfig?: { + access?: 'public' | 'restricted'; + directory?: string; + registry?: string; + alphaTypes?: string; + betaTypes?: string; + }; } export type ExtendedPackage = { diff --git a/packages/cli/src/lib/monorepo/entryPoints.ts b/packages/cli/src/lib/monorepo/entryPoints.ts new file mode 100644 index 0000000000..1e1fd7ed27 --- /dev/null +++ b/packages/cli/src/lib/monorepo/entryPoints.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { extname } from 'path'; +import { ExtendedPackageJSON } from './PackageGraph'; + +export interface EntryPoint { + mount: string; + path: string; + name: string; + ext: string; +} + +// Unless explicitly specified in exports, the index entrypoint is always +// assumed to be at src/index.ts for backwards compatibility. +const defaultIndex = { + mount: '.', + path: 'src/index.ts', + name: 'index', + ext: '.ts', +}; + +function parseEntryPoint(mount: string, path: string): EntryPoint { + let name = mount; + if (name === '.') { + name = 'index'; + } else if (name.startsWith('./')) { + name = name.slice(2); + } + if (name.includes('/')) { + throw new Error(`Mount point '${mount}' may not contain multiple slashes`); + } + + return { mount, path, name, ext: extname(path) }; +} + +export function readEntryPoints(pkg: ExtendedPackageJSON): Array<EntryPoint> { + const exp = pkg.exports; + if (typeof exp === 'string') { + return [defaultIndex]; + } else if (exp && typeof exp === 'object' && !Array.isArray(exp)) { + const entryPoints = new Array<{ + mount: string; + path: string; + name: string; + ext: string; + }>(); + + for (const mount of Object.keys(exp)) { + const path = exp[mount]; + if (typeof path !== 'string') { + throw new Error( + `Exports field value must be a string, got '${JSON.stringify(path)}'`, + ); + } + + entryPoints.push(parseEntryPoint(mount, path)); + } + + return entryPoints; + } + + return [defaultIndex]; +} diff --git a/packages/cli/src/lib/packager/copyPackageDist.ts b/packages/cli/src/lib/packager/copyPackageDist.ts deleted file mode 100644 index b704148542..0000000000 --- a/packages/cli/src/lib/packager/copyPackageDist.ts +++ /dev/null @@ -1,93 +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 fs from 'fs-extra'; -import npmPackList from 'npm-packlist'; -import { join as joinPath, resolve as resolvePath } from 'path'; - -const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes']; - -function resolveEntrypoint(pkg: any, name: string) { - const targetEntry = pkg.publishConfig[name] || pkg[name]; - return targetEntry && joinPath('..', targetEntry); -} - -// Writes e.g. alpha/package.json -async function writeReleaseStageEntrypoint( - pkg: any, - stage: 'alpha' | 'beta', - targetDir: string, -) { - await fs.ensureDir(resolvePath(targetDir, stage)); - await fs.writeJson( - resolvePath(targetDir, stage, 'package.json'), - { - name: pkg.name, - version: pkg.version, - main: resolveEntrypoint(pkg, 'main'), - module: resolveEntrypoint(pkg, 'module'), - browser: resolveEntrypoint(pkg, 'browser'), - types: joinPath('..', pkg.publishConfig[`${stage}Types`]), - }, - { encoding: 'utf8', spaces: 2 }, - ); -} - -export async function copyPackageDist(packageDir: string, targetDir: string) { - const pkgPath = resolvePath(packageDir, 'package.json'); - const pkgContent = await fs.readFile(pkgPath, 'utf8'); - const pkg = JSON.parse(pkgContent); - - const publishConfig = pkg.publishConfig ?? {}; - for (const key of Object.keys(publishConfig)) { - if (!SKIPPED_KEYS.includes(key)) { - pkg[key] = publishConfig[key]; - } - } - - // We remove the dependencies from package.json of packages that are marked - // as bundled, so that yarn doesn't try to install them. - if (pkg.bundled) { - delete pkg.dependencies; - delete pkg.devDependencies; - delete pkg.peerDependencies; - delete pkg.optionalDependencies; - } - - // Lists all dist files, respecting .npmignore, files field in package.json, etc. - const filePaths = await npmPackList({ - path: packageDir, - // This makes sure we use the update package.json when listing files - packageJsonCache: new Map([[resolvePath(packageDir, 'package.json'), pkg]]), - }); - - await fs.ensureDir(targetDir); - for (const filePath of filePaths.sort()) { - const target = resolvePath(targetDir, filePath); - if (filePath === 'package.json') { - await fs.writeJson(target, pkg, { encoding: 'utf8', spaces: 2 }); - } else { - await fs.copy(resolvePath(packageDir, filePath), target); - } - } - - if (publishConfig.alphaTypes) { - await writeReleaseStageEntrypoint(pkg, 'alpha', targetDir); - } - if (publishConfig.betaTypes) { - await writeReleaseStageEntrypoint(pkg, 'beta', targetDir); - } -} diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/lib/packager/createDistWorkspace.ts index 5451a873f7..2d2967fc44 100644 --- a/packages/cli/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/lib/packager/createDistWorkspace.ts @@ -37,7 +37,7 @@ import { getOutputsForRole, Output, } from '../builder'; -import { copyPackageDist } from './copyPackageDist'; +import { productionPack } from './productionPack'; import { getRoleInfo } from '../role'; import { runParallelWorkers } from '../parallel'; @@ -178,6 +178,7 @@ export async function createDistWorkspace( if (outputs.size > 0) { standardBuilds.push({ targetDir: pkg.dir, + packageJson: pkg.packageJson, outputs: outputs, logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, // No need to detect these for the backend builds, we assume no minification or types @@ -257,7 +258,10 @@ async function moveToDistWorkspace( const outputDir = relativePath(paths.targetRoot, target.dir); const absoluteOutputPath = resolvePath(workspaceDir, outputDir); - await copyPackageDist(target.dir, absoluteOutputPath); + await productionPack({ + packageDir: target.dir, + targetDir: absoluteOutputPath, + }); }), ); diff --git a/packages/cli/src/lib/packager/productionPack.ts b/packages/cli/src/lib/packager/productionPack.ts new file mode 100644 index 0000000000..b91112216a --- /dev/null +++ b/packages/cli/src/lib/packager/productionPack.ts @@ -0,0 +1,259 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import npmPackList from 'npm-packlist'; +import { join as joinPath, resolve as resolvePath } from 'path'; +import { ExtendedPackageJSON } from '../monorepo'; +import { readEntryPoints } from '../monorepo/entryPoints'; + +const PKG_PATH = 'package.json'; +const PKG_BACKUP_PATH = 'package.json-prepack'; + +const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes']; +const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx']; + +interface ProductionPackOptions { + packageDir: string; + targetDir?: string; +} + +export async function productionPack(options: ProductionPackOptions) { + const { packageDir, targetDir } = options; + const pkgPath = resolvePath(packageDir, PKG_PATH); + const pkgContent = await fs.readFile(pkgPath, 'utf8'); + const pkg = JSON.parse(pkgContent) as ExtendedPackageJSON; + + // If we're making the update in-line, back up the package.json + if (!targetDir) { + await fs.writeFile(PKG_BACKUP_PATH, pkgContent); + } + + const hasStageEntry = + !!pkg.publishConfig?.alphaTypes || !!pkg.publishConfig?.betaTypes; + if (pkg.exports && hasStageEntry) { + throw new Error( + 'Combining both exports and alpha/beta types is not supported', + ); + } + + // This mutates pkg to fill in index exports, so call it before applying publishConfig + const writeCompatibilityEntryPoints = await prepareExportsEntryPoints( + pkg, + packageDir, + ); + + // TODO(Rugvip): Once exports are rolled out more broadly we should deprecate and remove this behavior + const publishConfig = pkg.publishConfig ?? {}; + for (const key of Object.keys(publishConfig)) { + if (!SKIPPED_KEYS.includes(key)) { + (pkg as any)[key] = publishConfig[key as keyof typeof publishConfig]; + } + } + + // For published packages we rely on compatibility entry points rather than this + delete pkg.typesVersions; + + // We remove the dependencies from package.json of packages that are marked + // as bundled, so that yarn doesn't try to install them. + if (pkg.bundled) { + delete pkg.dependencies; + delete pkg.devDependencies; + delete pkg.peerDependencies; + delete pkg.optionalDependencies; + } + + if (targetDir) { + // Lists all dist files, respecting .npmignore, files field in package.json, etc. + const filePaths = await npmPackList({ + path: packageDir, + // This makes sure we use the updated package.json when listing files + packageJsonCache: new Map([ + [resolvePath(packageDir, PKG_PATH), pkg], + ]) as any, // Seems like this parameter type is wrong, + }); + + await fs.ensureDir(targetDir); + for (const filePath of filePaths.sort()) { + const target = resolvePath(targetDir, filePath); + if (filePath === PKG_PATH) { + await fs.writeJson(target, pkg, { encoding: 'utf8', spaces: 2 }); + } else { + await fs.copy(resolvePath(packageDir, filePath), target); + } + } + } else { + await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 }); + } + + if (publishConfig.alphaTypes) { + await writeReleaseStageEntrypoint(pkg, 'alpha', targetDir ?? packageDir); + } + if (publishConfig.betaTypes) { + await writeReleaseStageEntrypoint(pkg, 'beta', targetDir ?? packageDir); + } + if (writeCompatibilityEntryPoints) { + await writeCompatibilityEntryPoints(targetDir ?? packageDir); + } +} + +// Reverts the changes made by productionPack when called without a targetDir. +export async function revertProductionPack(packageDir: string) { + // postpack isn't called by yarn right now, so it needs to be called manually + try { + await fs.move(PKG_BACKUP_PATH, PKG_PATH, { overwrite: true }); + + // Check if we're shipping types for other release stages, clean up in that case + const pkg = await fs.readJson(PKG_PATH); + if (pkg.publishConfig?.alphaTypes) { + await fs.remove(resolvePath(packageDir, 'alpha')); + } + if (pkg.publishConfig?.betaTypes) { + await fs.remove(resolvePath(packageDir, 'beta')); + } + + // Remove any extra entrypoint backwards compatibility directories + const entryPoints = readEntryPoints(pkg); + for (const entryPoint of entryPoints) { + if (entryPoint.mount !== '.' && SCRIPT_EXTS.includes(entryPoint.ext)) { + await fs.remove(resolvePath(packageDir, entryPoint.name)); + } + } + } catch (error) { + console.warn( + `Failed to restore package.json, ${error}. ` + + 'Your package will be fine but you may have ended up with some garbage in the repo.', + ); + } +} + +function resolveEntrypoint(pkg: any, name: string) { + const targetEntry = pkg.publishConfig[name] || pkg[name]; + return targetEntry && joinPath('..', targetEntry); +} + +// Writes e.g. alpha/package.json +async function writeReleaseStageEntrypoint( + pkg: ExtendedPackageJSON, + stage: 'alpha' | 'beta', + targetDir: string, +) { + await fs.ensureDir(resolvePath(targetDir, stage)); + await fs.writeJson( + resolvePath(targetDir, stage, PKG_PATH), + { + name: pkg.name, + version: pkg.version, + main: resolveEntrypoint(pkg, 'main'), + module: resolveEntrypoint(pkg, 'module'), + browser: resolveEntrypoint(pkg, 'browser'), + types: joinPath('..', pkg.publishConfig![`${stage}Types`]!), + }, + { encoding: 'utf8', spaces: 2 }, + ); +} + +const EXPORT_MAP = { + import: '.esm.js', + require: '.cjs.js', + types: '.d.ts', +}; + +/** + * Rewrites the exports field in package.json to point to dist files, as + * well as returning a function that creates backwards compatibility + * entry points for importers that don't support exports. + */ +async function prepareExportsEntryPoints( + pkg: ExtendedPackageJSON, + packageDir: string, +) { + const distPath = resolvePath(packageDir, 'dist'); + if (!(await fs.pathExists(distPath))) { + return undefined; + } + const distFiles = await fs.readdir(distPath); + const outputExports = {} as Record<string, Record<string, string>>; + + const compatibilityWriters = new Array< + (targetDir: string) => Promise<void> + >(); + + const entryPoints = readEntryPoints(pkg); + for (const entryPoint of entryPoints) { + if (!SCRIPT_EXTS.includes(entryPoint.ext)) { + continue; + } + const exp = {} as Record<string, string>; + for (const [key, ext] of Object.entries(EXPORT_MAP)) { + const name = `${entryPoint.name}${ext}`; + if (distFiles.includes(name)) { + exp[key] = `./${joinPath(`dist`, name)}`; + } + } + exp.default = exp.require ?? exp.import; + + // This creates a directory with a lone package.json for backwards compatibility + if (entryPoint.mount === '.') { + if (exp.default) { + pkg.main = exp.default; + } + if (exp.import) { + pkg.module = exp.import; + } + if (exp.types) { + pkg.types = exp.types; + } + } else { + // This is deferred until after we have created the target directory + compatibilityWriters.push(async targetDir => { + const entryPointDir = resolvePath(targetDir, entryPoint.name); + await fs.ensureDir(entryPointDir); + await fs.writeJson( + resolvePath(entryPointDir, PKG_PATH), + { + name: pkg.name, + version: pkg.version, + ...(exp.default ? { main: joinPath('..', exp.default) } : {}), + ...(exp.import ? { module: joinPath('..', exp.import) } : {}), + ...(exp.types ? { types: joinPath('..', exp.types) } : {}), + }, + { encoding: 'utf8', spaces: 2 }, + ); + }); + if (Array.isArray(pkg.files) && !pkg.files.includes(entryPoint.name)) { + pkg.files.push(entryPoint.name); + } + } + + if (Object.keys(exp).length > 0) { + outputExports[entryPoint.mount] = exp; + } + } + + if (pkg.exports) { + pkg.exports = outputExports; + // We treat package.json as a fixed export that is always available in the published package + pkg.exports['./package.json'] = './package.json'; + } + + if (compatibilityWriters.length > 0) { + return async (targetDir: string) => { + await Promise.all(compatibilityWriters.map(writer => writer(targetDir))); + }; + } + return undefined; +} diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli/src/lib/version.test.ts index fb9bf83e7c..604129679b 100644 --- a/packages/cli/src/lib/version.test.ts +++ b/packages/cli/src/lib/version.test.ts @@ -17,7 +17,6 @@ import mockFs from 'mock-fs'; import { packageVersions, createPackageVersionProvider } from './version'; import { Lockfile } from './versioning'; -// eslint-disable-next-line monorepo/no-internal-import import corePluginApiPkg from '@backstage/core-plugin-api/package.json'; describe('createPackageVersionProvider', () => { diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 5a199236f5..bf89842d5f 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -19,7 +19,7 @@ import semver from 'semver'; import { paths } from './paths'; import { Lockfile } from './versioning'; -/* eslint-disable monorepo/no-relative-import */ +/* eslint-disable @backstage/no-relative-monorepo-imports */ /* This is a list of all packages used by the templates. If dependencies are added or removed, diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index fedbe61f02..1222b2b4b8 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -258,3 +258,5 @@ declare module 'webpack-node-externals' { } } } + +declare module '@esbuild-kit/cjs-loader' {} diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 3a9091e87f..2b860260ac 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -30,8 +30,8 @@ "dependencies": { "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", "@backstage/config": "{{versionQuery '@backstage/config'}}", - "@types/express": "{{versionQuery '@types/express' '4.17.13'}}", - "express": "{{versionQuery 'express' '4.18.1'}}", + "@types/express": "{{versionQuery '@types/express' '4.17.6'}}", + "express": "{{versionQuery 'express' '4.17.1'}}", "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", "winston": "{{versionQuery 'winston' '3.2.1'}}", "node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}", @@ -41,7 +41,7 @@ "@backstage/cli": "{{versionQuery '@backstage/cli'}}", "@types/supertest": "{{versionQuery '@types/supertest' '2.0.12'}}", "supertest": "{{versionQuery 'supertest' '6.2.4'}}", - "msw": "{{versionQuery 'msw' '0.49.0'}}" + "msw": "{{versionQuery 'msw' '1.0.0'}}" }, "files": [ "dist" diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index d319b16449..de42f3cdef 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -48,7 +48,7 @@ "@testing-library/react": "{{versionQuery '@testing-library/react' '12.1.3'}}", "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}", "@types/node": "{{versionQuery '@types/node' '16.11.26'}}", - "msw": "{{versionQuery 'msw' '0.49.0'}}", + "msw": "{{versionQuery 'msw' '1.0.0'}}", "cross-fetch": "{{versionQuery 'cross-fetch' '3.1.5'}}" }, "files": [ diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs index 159fcd70c5..03b25dfb4f 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs @@ -1,7 +1,7 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import { Table, TableColumn, Progress } from '@backstage/core-components'; -import Alert from '@material-ui/lab/Alert'; +import { Table, TableColumn, Progress, ResponseErrorPanel } from '@backstage/core-components'; +import { fetchApiRef, useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles({ @@ -74,6 +74,7 @@ export const DenseTable = ({ users }: DenseTableProps) => { }; export const ExampleFetchComponent = () => { + const { fetch } = useApi(fetchApiRef); const { value, loading, error } = useAsync(async (): Promise<User[]> => { const response = await fetch('https://randomuser.me/api/?results=20'); const data = await response.json(); @@ -83,7 +84,7 @@ export const ExampleFetchComponent = () => { if (loading) { return <Progress />; } else if (error) { - return <Alert severity="error">{error.message}</Alert>; + return <ResponseErrorPanel error={error} />; } return <DenseTable users={value || []} />; diff --git a/packages/codemods/.eslintrc.js b/packages/codemods/.eslintrc.js index f9fb2e27c9..e2a53a6ad2 100644 --- a/packages/codemods/.eslintrc.js +++ b/packages/codemods/.eslintrc.js @@ -1,14 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - 'no-console': 0, - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: false, - peerDependencies: false, - bundledDependencies: false, - }, - ], - }, -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 2481aff8b1..c3387fce64 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/codemods +## 0.1.43 + +### Patch Changes + +- 02f1316e57: Moved `commander` to being a regular dependency. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## 0.1.43-next.0 + +### Patch Changes + +- 02f1316e57: Moved `commander` to being a regular dependency. +- Updated dependencies + - @backstage/cli-common@0.1.11 + ## 0.1.42 ### Patch Changes diff --git a/packages/codemods/README.md b/packages/codemods/README.md index c40e9a8dcc..791612c06b 100644 --- a/packages/codemods/README.md +++ b/packages/codemods/README.md @@ -35,4 +35,4 @@ npx jscodeshift --parser=tsx --extensions=tsx,js,ts,tsx --transform=node_modules ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/codemods/package.json b/packages/codemods/package.json index cbcc0613d4..17649d49cf 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.42", + "version": "0.1.43", "publishConfig": { "access": "public", "main": "dist/index.cjs.js" @@ -35,6 +35,7 @@ "dependencies": { "@backstage/cli-common": "workspace:^", "chalk": "^4.0.0", + "commander": "^9.1.0", "jscodeshift": "^0.14.0", "jscodeshift-add-imports": "^1.0.10" }, @@ -42,7 +43,6 @@ "@backstage/cli": "workspace:^", "@types/jscodeshift": "^0.11.0", "@types/node": "^16.11.26", - "commander": "^9.1.0", "ts-node": "^10.0.0" }, "nodemonConfig": { diff --git a/packages/config-loader/README.md b/packages/config-loader/README.md index 261394a6a5..ccb792404d 100644 --- a/packages/config-loader/README.md +++ b/packages/config-loader/README.md @@ -9,4 +9,4 @@ Do not install this package directly, it is an internal package used by [@backst ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index c78eb632da..657c6630eb 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -56,7 +56,7 @@ "@types/node": "^16.11.26", "@types/yup": "^0.29.13", "mock-fs": "^5.1.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/packages/config/README.md b/packages/config/README.md index 9866b1bce9..4c130ef6a3 100644 --- a/packages/config/README.md +++ b/packages/config/README.md @@ -9,4 +9,4 @@ Do not install this package directly, it is an internal package used by [@backst ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 0d059f7b46..4575040798 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/core-app-api +## 1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + +## 1.5.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- dff4d8ddb1: Fixed an issue where an explicit port the frontend base URL could break the app. +- Updated dependencies + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + +## 1.4.1-next.0 + +### Patch Changes + +- dff4d8ddb1: Fixed an issue where an explicit port the frontend base URL could break the app. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + ## 1.4.0 ### Minor Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index f7e2155db0..405c9ed7b8 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -22,6 +22,7 @@ import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstageIdentityResponse } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; +import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; import { ConfigApi } from '@backstage/core-plugin-api'; @@ -282,6 +283,25 @@ export class BitbucketAuth { static create(options: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T; } +// @public +export class BitbucketServerAuth { + // (undocumented) + static create( + options: OAuthApiCreateOptions, + ): typeof bitbucketServerAuthApiRef.T; +} + +// @public +export type BitbucketServerSession = { + providerInfo: { + accessToken: string; + scopes: Set<string>; + expiresAt?: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; + // @public export type BitbucketSession = { providerInfo: { diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 330405d853..c43b27009b 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "1.4.0", + "version": "1.5.1-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -57,7 +57,7 @@ "@types/node": "^16.11.26", "@types/zen-observable": "^0.8.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0", + "msw": "^1.0.0", "react-router-beta": "npm:react-router@6.0.0-beta.0", "react-router-dom-beta": "npm:react-router-dom@6.0.0-beta.0", "react-router-dom-stable": "npm:react-router-dom@^6.3.0", diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts new file mode 100644 index 0000000000..32e7755a00 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts @@ -0,0 +1,51 @@ +/* + * 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 MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; +import { UrlPatternDiscovery } from '../../DiscoveryApi'; +import BitbucketServerAuth from './BitbucketServerAuth'; + +const getSession = jest.fn(); + +jest.mock('../../../../lib/AuthSessionManager', () => ({ + ...(jest.requireActual('../../../../lib/AuthSessionManager') as any), + RefreshingAuthSessionManager: class { + getSession = getSession; + }, +})); + +describe('BitbucketServerAuth', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it.each([ + ['PUBLIC_REPOS', ['PUBLIC_REPOS']], + ['PROJECT_ADMIN REPO_READ', ['PROJECT_ADMIN', 'REPO_READ']], + [ + 'PROJECT_ADMIN REPO_READ ACCOUNT_WRITE', + ['PROJECT_ADMIN', 'REPO_READ', 'ACCOUNT_WRITE'], + ], + ])(`should normalize scopes correctly - %p`, (scope, scopes) => { + const bitbucketServerAuth = BitbucketServerAuth.create({ + oauthRequestApi: new MockOAuthApi(), + discoveryApi: UrlPatternDiscovery.compile('http://example.com'), + }); + + bitbucketServerAuth.getAccessToken(scope); + expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) }); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts new file mode 100644 index 0000000000..dc50eff6ab --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts @@ -0,0 +1,66 @@ +/* + * 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 { + BackstageIdentityResponse, + bitbucketServerAuthApiRef, + ProfileInfo, +} from '@backstage/core-plugin-api'; + +import { OAuthApiCreateOptions } from '../types'; +import { OAuth2 } from '../oauth2'; + +export type BitbucketServerAuthResponse = { + providerInfo: { + accessToken: string; + scope: string; + expiresInSeconds: number; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; + +const DEFAULT_PROVIDER = { + id: 'bitbucketServer', + title: 'Bitbucket Server', + icon: () => null, +}; + +/** + * Implements the OAuth flow to Bitbucket Server. + * @public + */ +export default class BitbucketServerAuth { + static create( + options: OAuthApiCreateOptions, + ): typeof bitbucketServerAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['PROJECT_ADMIN'], + } = options; + + return OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider, + environment, + defaultScopes, + }); + } +} diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/index.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/index.ts new file mode 100644 index 0000000000..5fcb8f72f2 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/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 * from './types'; +export { default as BitbucketServerAuth } from './BitbucketServerAuth'; diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/types.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/types.ts new file mode 100644 index 0000000000..1041aa8110 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ProfileInfo, + BackstageIdentityResponse, +} from '@backstage/core-plugin-api'; + +/** + * Session information for Bitbucket Server auth. + * + * @public + */ +export type BitbucketServerSession = { + providerInfo: { + accessToken: string; + scopes: Set<string>; + expiresAt?: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; 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 c4cf520db5..b54e0424da 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -23,5 +23,6 @@ export * from './saml'; export * from './microsoft'; export * from './onelogin'; export * from './bitbucket'; +export * from './bitbucketServer'; export * from './atlassian'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types'; diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 1b2ac9471a..db0d3ea9af 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -634,6 +634,28 @@ describe('Integration Test', () => { }, }, ], + [ + ['http://localhost', 'http://localhost'], + { + backend: { + baseUrl: 'http://localhost:80', + }, + app: { + baseUrl: 'http://localhost:80', + }, + }, + ], + [ + ['http://localhost', 'http://localhost'], + { + backend: { + baseUrl: 'http://localhost', + }, + app: { + baseUrl: 'http://localhost', + }, + }, + ], [ ['http://localhost/backstage', 'http://localhost/backstage'], { @@ -674,7 +696,7 @@ describe('Integration Test', () => { }, ], ])( - 'should be %p when %p', + 'should be %p when %p (%#)', async ([expectedAppUrl, expectedBackendUrl], data) => { const app = new AppManager({ apis: [], diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index d5f8210185..33d43cbca6 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -82,6 +82,17 @@ type CompatiblePlugin = output(): Array<{ type: 'feature-flag'; name: string }>; }); +/** + * Creates a base URL that uses to the current document origin. + */ +function createLocalBaseUrl(fullUrl: string): string { + const url = new URL(fullUrl); + url.protocol = document.location.protocol; + url.hostname = document.location.hostname; + url.port = document.location.port; + return url.toString().replace(/\/$/, ''); +} + function useConfigLoader( configLoader: AppConfigLoader | undefined, components: AppComponents, @@ -121,27 +132,6 @@ function useConfigLoader( if (config.value?.length) { const urlConfigReader = ConfigReader.fromConfigs(config.value); - /** - * Return the origin of the given URL. - * @param url An absolute URL. - * @returns The given URL's origin. - * @throws If fullUrl is not a correctly formatted absolute URL. - */ - const getOrigin = (url: string) => new URL(url).origin; - - /** - * Resolve an absolute URL as relative to the current document. - * @param fullUrl URL to resolve. - * @returns Absolute URL with origin as the current document origin. - * @throws If fullUrl is not a correctly formatted absolute URL. - */ - const overrideOrigin = (fullUrl: string) => { - return new URL( - fullUrl.replace(getOrigin(fullUrl), ''), - document.location.origin, - ).href.replace(/\/$/, ''); - }; - /** * Test configs may not define `app.baseUrl` or `backend.baseUrl` and we * don't want to enforce here. @@ -155,18 +145,18 @@ function useConfigLoader( context: 'relative-resolver', }; if (appBaseUrl && backendBaseUrl) { - const appOrigin = getOrigin(appBaseUrl); - const backendOrigin = getOrigin(backendBaseUrl); + const appOrigin = new URL(appBaseUrl).origin; + const backendOrigin = new URL(backendBaseUrl).origin; if (appOrigin === backendOrigin) { - const newBackendBaseUrl = overrideOrigin(backendBaseUrl); + const newBackendBaseUrl = createLocalBaseUrl(backendBaseUrl); if (backendBaseUrl !== newBackendBaseUrl) { relativeResolverConfig.data.backend = { baseUrl: newBackendBaseUrl }; } } } if (appBaseUrl) { - const newAppBaseUrl = overrideOrigin(appBaseUrl); + const newAppBaseUrl = createLocalBaseUrl(appBaseUrl); if (appBaseUrl !== newAppBaseUrl) { relativeResolverConfig.data.app = { baseUrl: newAppBaseUrl }; } diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index a46acb0897..5ad035fbfd 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/core-components +## 0.12.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/version-bridge@1.0.3 + +## 0.12.4 + +### Patch Changes + +- 68ce7d0417: Added aria labels on the support button and sidebar +- 5637ebed92: Added a global override for `window.open` that helps prevent security vulnerabilities. +- 910015f5b7: The Button component has been deprecated in favor of the LinkButton component +- 20840b36b4: Adds new type, TableOptions, extending Material Table Options. +- e81a6e0ab5: Updated Link URL validation to be more strict. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 66e2aab4c4: Navigation items in mobile sidebar now have aria label. +- Updated dependencies + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/version-bridge@1.0.3 + +## 0.12.4-next.1 + +### Patch Changes + +- 66e2aab4c4: Navigation items in mobile sidebar now have aria label. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.3 + +## 0.12.4-next.0 + +### Patch Changes + +- 910015f5b7: The Button component has been deprecated in favor of the LinkButton component +- 20840b36b4: Adds new type, TableOptions, extending Material Table Options. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.3 + ## 0.12.3 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 4cdc42ee79..6439be23a7 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -31,6 +31,7 @@ import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Options } from 'react-markdown'; +import { Options as Options_2 } from '@material-table/core'; import { Overrides } from '@material-ui/core/styles/overrides'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -129,11 +130,11 @@ export type BreadcrumbsStyledBoxClassKey = 'root'; // @public export function BrokenImageIcon(props: IconComponentProps): JSX.Element; -// @public -export const Button: (props: ButtonProps) => JSX.Element; +// @public @deprecated (undocumented) +export const Button: (props: LinkButtonProps) => JSX.Element; -// @public -export type ButtonProps = ButtonProps_2 & Omit<LinkProps, 'variant' | 'color'>; +// @public @deprecated (undocumented) +export type ButtonProps = LinkButtonProps; // @public (undocumented) export type CardActionsTopRightClassKey = 'root'; @@ -629,6 +630,13 @@ export function LinearGauge(props: Props_11): JSX.Element | null; // @public export const Link: (props: LinkProps) => JSX.Element; +// @public +export const LinkButton: (props: LinkButtonProps) => JSX.Element; + +// @public +export type LinkButtonProps = ButtonProps_2 & + Omit<LinkProps, 'variant' | 'color'>; + // Warning: (ae-missing-release-tag) "LinkProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1392,6 +1400,11 @@ export type TableFiltersClassKey = 'root' | 'value' | 'heder' | 'filters'; // @public (undocumented) export type TableHeaderClassKey = 'header'; +// Warning: (ae-missing-release-tag) "TableOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface TableOptions<T extends object = {}> extends Options_2<T> {} + // Warning: (ae-missing-release-tag) "TableProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 72f8b069bf..aa47734d8b 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.12.3", + "version": "0.12.5-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -98,7 +98,7 @@ "@types/react-window": "^1.8.5", "@types/zen-observable": "^0.8.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/packages/core-components/src/components/Avatar/utils.ts b/packages/core-components/src/components/Avatar/utils.ts index d8310f18c0..4f71d736f1 100644 --- a/packages/core-components/src/components/Avatar/utils.ts +++ b/packages/core-components/src/components/Avatar/utils.ts @@ -22,11 +22,11 @@ export function stringToColor(str: string) { let color = '#'; for (let i = 0; i < 3; i++) { const value = (hash >> (i * 8)) & 0xff; - color += `00${value.toString(16)}`.substr(-2); + color += `00${value.toString(16)}`.slice(-2); } return color; } export function extractInitials(value: string) { - return value.match(/\b\w/g)?.join('').substring(0, 2); + return value.match(/\b\w/g)?.join('').slice(0, 2); } diff --git a/packages/core-components/src/components/Link/Link.test.tsx b/packages/core-components/src/components/Link/Link.test.tsx index 3538b79052..682c4bea8a 100644 --- a/packages/core-components/src/components/Link/Link.test.tsx +++ b/packages/core-components/src/components/Link/Link.test.tsx @@ -173,4 +173,24 @@ describe('<Link />', () => { }); }); }); + + it('throws an error when attempting to link to script code', () => { + expect(() => + // eslint-disable-next-line no-script-url + render(wrapInTestApp(<Link to="javascript:alert('hello')">Script</Link>)), + ).toThrowErrorMatchingInlineSnapshot( + `"Link component rejected javascript: URL as a security precaution"`, + ); + }); +}); + +describe('window.open', () => { + it('throws an error when attempting to open script code', () => { + expect(() => + // eslint-disable-next-line no-script-url + window.open("javascript:alert('hello')"), + ).toThrowErrorMatchingInlineSnapshot( + `"Rejected window.open() with a javascript: URL as a security precaution"`, + ); + }); }); diff --git a/packages/core-components/src/components/Link/Link.tsx b/packages/core-components/src/components/Link/Link.tsx index 440fcb811f..be91753ea3 100644 --- a/packages/core-components/src/components/Link/Link.tsx +++ b/packages/core-components/src/components/Link/Link.tsx @@ -55,6 +55,32 @@ const useStyles = makeStyles( export const isExternalUri = (uri: string) => /^([a-z+.-]+):/.test(uri); +// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js +const scriptProtocolPattern = + // eslint-disable-next-line no-control-regex + /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; + +// We install this globally in order to prevent javascript: URL XSS attacks via window.open +const originalWindowOpen = window.open as typeof window.open & { + __backstage?: true; +}; +if (originalWindowOpen && !originalWindowOpen.__backstage) { + const newOpen = function open( + this: Window, + ...args: Parameters<typeof window.open> + ) { + const url = String(args[0]); + if (scriptProtocolPattern.test(url)) { + throw new Error( + 'Rejected window.open() with a javascript: URL as a security precaution', + ); + } + return originalWindowOpen.apply(this, args); + }; + newOpen.__backstage = true; + window.open = newOpen; +} + export type LinkProps = Omit<MaterialLinkProps, 'to'> & Omit<RouterLinkProps, 'to'> & { to: string; @@ -144,6 +170,12 @@ export const Link = React.forwardRef<any, LinkProps>( const external = isExternalUri(to); const newWindow = external && !!/^https?:/.exec(to); + if (scriptProtocolPattern.test(to)) { + throw new Error( + 'Link component rejected javascript: URL as a security precaution', + ); + } + const handleClick = (event: React.MouseEvent<any, MouseEvent>) => { onClick?.(event); if (!noTrack) { diff --git a/packages/core-components/src/components/Button/Button.stories.tsx b/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx similarity index 89% rename from packages/core-components/src/components/Button/Button.stories.tsx rename to packages/core-components/src/components/LinkButton/LinkButton.stories.tsx index ca95316ee8..3dc49abb7f 100644 --- a/packages/core-components/src/components/Button/Button.stories.tsx +++ b/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { ComponentType } from 'react'; -import { Button } from './Button'; +import { LinkButton } from './LinkButton'; import { useLocation } from 'react-router-dom'; import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api'; import Divider from '@material-ui/core/Divider'; @@ -37,7 +37,7 @@ const Location = () => { export default { title: 'Inputs/Button', - component: Button, + component: LinkButton, decorators: [ (Story: ComponentType<{}>) => wrapInTestApp( @@ -80,9 +80,9 @@ export const Default = () => { <pre>color="primary" variant="contained"</pre> </ListItemText> - <Button to={link()} color="primary" variant="contained"> + <LinkButton to={link()} color="primary" variant="contained"> Register Component - </Button> + </LinkButton> </ListItem> <ListItem> <ListItemText> @@ -93,9 +93,9 @@ export const Default = () => { <pre>color="secondary" variant="contained"</pre> </ListItemText> - <Button to={link()} color="secondary" variant="contained"> + <LinkButton to={link()} color="secondary" variant="contained"> Cancel - </Button> + </LinkButton> </ListItem> <ListItem> <ListItemText> @@ -106,9 +106,9 @@ export const Default = () => { <pre>color="default" variant="outlined"</pre> </ListItemText> - <Button to={link()} color="default" variant="outlined"> + <LinkButton to={link()} color="default" variant="outlined"> View Details - </Button> + </LinkButton> </ListItem> </List> ); @@ -128,17 +128,17 @@ export const ButtonLinks = () => { // TODO: Refactor to use new routing mechanisms } <ListItem> - <Button to={link()} color="default" variant="outlined"> + <LinkButton to={link()} color="default" variant="outlined"> Route Ref - </Button> + </LinkButton>   has props for both Material-UI's component as well as for react-router-dom's Route object. </ListItem> <ListItem> - <Button to="/staticpath" color="default" variant="outlined"> + <LinkButton to="/staticpath" color="default" variant="outlined"> Static Path - </Button> + </LinkButton>   links to a statically defined route. In general, this should be avoided. </ListItem> diff --git a/packages/core-components/src/components/Button/Button.test.tsx b/packages/core-components/src/components/LinkButton/LinkButton.test.tsx similarity index 84% rename from packages/core-components/src/components/Button/Button.test.tsx rename to packages/core-components/src/components/LinkButton/LinkButton.test.tsx index c5942e3d78..7dbc42f9cb 100644 --- a/packages/core-components/src/components/Button/Button.test.tsx +++ b/packages/core-components/src/components/LinkButton/LinkButton.test.tsx @@ -17,17 +17,17 @@ import React from 'react'; import { render, fireEvent, act } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; -import { Button } from './Button'; +import { LinkButton } from './LinkButton'; import { Route, Routes } from 'react-router-dom'; -describe('<Button />', () => { +describe('<LinkButton />', () => { it('navigates using react-router', async () => { const testString = 'This is test string'; - const buttonLabel = 'Navigate!'; + const linkButtonLabel = 'Navigate!'; const { getByText } = render( wrapInTestApp( <> - <Button to="/test">{buttonLabel}</Button> + <LinkButton to="/test">{linkButtonLabel}</LinkButton> <Routes> <Route path="/test" element={<p>{testString}</p>} /> </Routes> @@ -37,7 +37,7 @@ describe('<Button />', () => { expect(() => getByText(testString)).toThrow(); await act(async () => { - fireEvent.click(getByText(buttonLabel)); + fireEvent.click(getByText(linkButtonLabel)); }); expect(getByText(testString)).toBeInTheDocument(); }); diff --git a/packages/core-components/src/components/Button/Button.tsx b/packages/core-components/src/components/LinkButton/LinkButton.tsx similarity index 72% rename from packages/core-components/src/components/Button/Button.tsx rename to packages/core-components/src/components/LinkButton/LinkButton.tsx index 575f42b03c..7da25bcb41 100644 --- a/packages/core-components/src/components/Button/Button.tsx +++ b/packages/core-components/src/components/LinkButton/LinkButton.tsx @@ -21,14 +21,14 @@ import React from 'react'; import { Link, LinkProps } from '../Link'; /** - * Properties for {@link Button} + * Properties for {@link LinkButton} * * @public * @remarks * * See {@link https://v4.mui.com/api/button/#props | Material-UI Button Props} for all properties */ -export type ButtonProps = MaterialButtonProps & +export type LinkButtonProps = MaterialButtonProps & Omit<LinkProps, 'variant' | 'color'>; /** @@ -43,9 +43,21 @@ const LinkWrapper = React.forwardRef<any, LinkProps>((props, ref) => ( * * @public * @remarks - * - * Makes the Button to utilize react-router */ -export const Button = React.forwardRef<any, ButtonProps>((props, ref) => ( - <MaterialButton ref={ref} component={LinkWrapper} {...props} /> -)) as (props: ButtonProps) => JSX.Element; +export const LinkButton = React.forwardRef<any, LinkButtonProps>( + (props, ref) => ( + <MaterialButton ref={ref} component={LinkWrapper} {...props} /> + ), +) as (props: LinkButtonProps) => JSX.Element; + +/** + * @public + * @deprecated use LinkButton instead + */ +export const Button = LinkButton; + +/** + * @public + * @deprecated use LinkButtonProps instead + */ +export type ButtonProps = LinkButtonProps; diff --git a/packages/core-components/src/components/LinkButton/index.ts b/packages/core-components/src/components/LinkButton/index.ts new file mode 100644 index 0000000000..848203c2dc --- /dev/null +++ b/packages/core-components/src/components/LinkButton/index.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. + */ +export { LinkButton } from './LinkButton'; +export type { LinkButtonProps } from './LinkButton'; + +export { Button } from './LinkButton'; +export type { ButtonProps } from './LinkButton'; diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 8f3a2ce65c..c8ff5cbceb 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -15,7 +15,6 @@ */ import React, { useMemo } from 'react'; -import Typography from '@material-ui/core/Typography'; import { AnsiChunk, AnsiLine, ChunkModifiers } from './AnsiProcessor'; import startCase from 'lodash/startCase'; import classnames from 'classnames'; @@ -159,8 +158,8 @@ export function LogLine({ const elements = useMemo( () => chunks.map(({ text, modifiers, highlight }, index) => ( - <Typography - component="span" + // eslint-disable-next-line react/forbid-elements + <span key={index} className={classnames( getModifierClasses(classes, modifiers), @@ -171,7 +170,7 @@ export function LogLine({ )} > {text} - </Typography> + </span> )), [chunks, highlightResultIndex, classes], ); diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx index dd707778e1..e1aac76381 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx @@ -18,7 +18,7 @@ import React, { ReactNode } from 'react'; import userEvent from '@testing-library/user-event'; import { renderInTestApp } from '@backstage/test-utils'; import { RealLogViewer } from './RealLogViewer'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import copyToClipboard from 'copy-to-clipboard'; // Used by useCopyToClipboard diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx index 7e00d50f33..93a6dfd7e9 100644 --- a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx @@ -20,7 +20,7 @@ import { TestApiProvider, MockErrorApi } from '@backstage/test-utils'; import { errorApiRef } from '@backstage/core-plugin-api'; import { AnsiLine } from './AnsiProcessor'; import { useLogViewerSelection } from './useLogViewerSelection'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import copyToClipboard from 'copy-to-clipboard'; // Used by useCopyToClipboard diff --git a/packages/core-components/src/components/SupportButton/SupportButton.tsx b/packages/core-components/src/components/SupportButton/SupportButton.tsx index 7bb9aab9b5..35cf228f52 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.tsx @@ -16,8 +16,6 @@ import { useApp } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; -import { makeStyles } from '@material-ui/core/styles'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; import DialogActions from '@material-ui/core/DialogActions'; @@ -27,7 +25,9 @@ import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import Popover from '@material-ui/core/Popover'; +import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; import React, { MouseEventHandler, useState } from 'react'; import { SupportItem, SupportItemLink, useSupportConfig } from '../../hooks'; import { HelpIcon } from '../../icons'; @@ -111,13 +111,14 @@ export function SupportButton(props: SupportButtonProps) { size="small" onClick={onClickHandler} data-testid="support-button" + aria-label="Support" > <HelpIcon /> </IconButton> ) : ( <Button data-testid="support-button" - aria-label="support" + aria-label="Support" color="primary" onClick={onClickHandler} startIcon={<HelpIcon />} @@ -156,7 +157,11 @@ export function SupportButton(props: SupportButtonProps) { ))} </List> <DialogActions> - <Button color="primary" onClick={popoverCloseHandler}> + <Button + color="primary" + onClick={popoverCloseHandler} + aria-label="Close" + > Close </Button> </DialogActions> diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx index c799402cbc..89a1d69b96 100644 --- a/packages/core-components/src/components/Table/Table.tsx +++ b/packages/core-components/src/components/Table/Table.tsx @@ -240,6 +240,8 @@ export interface TableProps<T extends object = {}> onStateChange?: (state: TableState) => any; } +export interface TableOptions<T extends object = {}> extends Options<T> {} + export function TableToolbar(toolbarProps: { toolbarRef: MutableRefObject<any>; setSearch: (value: string) => void; diff --git a/packages/core-components/src/components/Table/index.ts b/packages/core-components/src/components/Table/index.ts index 7fc245d6d0..00e1ae9de5 100644 --- a/packages/core-components/src/components/Table/index.ts +++ b/packages/core-components/src/components/Table/index.ts @@ -22,6 +22,7 @@ export type { TableColumn, TableFilter, TableProps, + TableOptions, TableState, TableClassKey, FiltersContainerClassKey, diff --git a/packages/core-components/src/components/index.ts b/packages/core-components/src/components/index.ts index 473fab8444..0057847754 100644 --- a/packages/core-components/src/components/index.ts +++ b/packages/core-components/src/components/index.ts @@ -16,7 +16,7 @@ export * from './AlertDisplay'; export * from './Avatar'; -export * from './Button'; +export * from './LinkButton'; export * from './CodeSnippet'; export * from './CopyTextButton'; export * from './CreateButton'; @@ -30,6 +30,7 @@ export * from './HeaderIconLinkRow'; export * from './HorizontalScrollGrid'; export * from './Lifecycle'; export * from './Link'; +export * from './LinkButton'; export * from './LogViewer'; export * from './MarkdownContent'; export * from './OAuthRequestDialog'; diff --git a/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx b/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx index da430d2f88..037e170640 100644 --- a/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx @@ -16,7 +16,7 @@ import Typography from '@material-ui/core/Typography'; import React, { ComponentClass, Component, ErrorInfo } from 'react'; -import { Button } from '../../components/Button'; +import { LinkButton } from '../../components/LinkButton'; import { ErrorPanel } from '../../components/ErrorPanel'; type SlackChannel = { @@ -49,9 +49,9 @@ const SlackLink = (props: { slackChannel?: string | SlackChannel }) => { } return ( - <Button to={slackChannel.href} variant="contained"> + <LinkButton to={slackChannel.href} variant="contained"> {slackChannel.name} - </Button> + </LinkButton> ); }; diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx index aac062c01b..48e0a8906a 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx @@ -104,7 +104,7 @@ export function HeaderTabs(props: HeaderTabsProps) { textColor="inherit" variant="scrollable" scrollButtons="auto" - aria-label="scrollable auto tabs example" + aria-label="tabs" onChange={handleChange} value={selectedTab} > diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.tsx index 95d474aae4..3d8d0f3bb0 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.tsx @@ -188,12 +188,12 @@ export function InfoCard(props: Props): JSX.Element { variants.forEach(name => { calculatedStyle = { ...calculatedStyle, - ...VARIANT_STYLES.card[name as keyof typeof VARIANT_STYLES['card']], + ...VARIANT_STYLES.card[name as keyof (typeof VARIANT_STYLES)['card']], }; calculatedCardStyle = { ...calculatedCardStyle, ...VARIANT_STYLES.cardContent[ - name as keyof typeof VARIANT_STYLES['cardContent'] + name as keyof (typeof VARIANT_STYLES)['cardContent'] ], }; }); diff --git a/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx b/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx index f7df3d43cc..1ba3dec3fd 100644 --- a/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx +++ b/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx @@ -22,7 +22,7 @@ import CardMedia from '@material-ui/core/CardMedia'; import Typography from '@material-ui/core/Typography'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { Button } from '../../components'; +import { LinkButton } from '../../components'; import { ItemCardGrid } from './ItemCardGrid'; import { ItemCardHeader } from './ItemCardHeader'; @@ -63,9 +63,9 @@ export const Default = () => ( .join(' ')} </CardContent> <CardActions> - <Button color="primary" to="/catalog"> + <LinkButton color="primary" to="/catalog"> Go There! - </Button> + </LinkButton> </CardActions> </Card> ))} @@ -99,9 +99,9 @@ export const Styling = () => { .join(' ')} </CardContent> <CardActions> - <Button color="primary" to="/catalog"> + <LinkButton color="primary" to="/catalog"> Go There! - </Button> + </LinkButton> </CardActions> </Card> ))} diff --git a/packages/core-components/src/layout/ItemCard/ItemCard.tsx b/packages/core-components/src/layout/ItemCard/ItemCard.tsx index fedcf7de8f..0b1ee34c0c 100644 --- a/packages/core-components/src/layout/ItemCard/ItemCard.tsx +++ b/packages/core-components/src/layout/ItemCard/ItemCard.tsx @@ -21,7 +21,7 @@ import CardContent from '@material-ui/core/CardContent'; import CardMedia from '@material-ui/core/CardMedia'; import Chip from '@material-ui/core/Chip'; import React, { ReactNode } from 'react'; -import { Button } from '../../components'; +import { LinkButton } from '../../components'; import { ItemCardHeader } from './ItemCardHeader'; type ItemCardProps = { @@ -80,14 +80,14 @@ export function ItemCard(props: ItemCardProps) { </CardContent> <CardActions> {!href && ( - <Button to="#" onClick={onClick} color="primary"> + <LinkButton to="#" onClick={onClick} color="primary"> {label} - </Button> + </LinkButton> )} {href && ( - <Button to={href} color="primary"> + <LinkButton to={href} color="primary"> {label} - </Button> + </LinkButton> )} </CardActions> </Card> diff --git a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx index 5254199563..1eb1423c61 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx @@ -23,7 +23,7 @@ import { makeStyles } from '@material-ui/core/styles'; import React, { useContext } from 'react'; import { useLocation } from 'react-router-dom'; import { Link } from '../../components'; -import { SidebarConfigContext, SidebarConfig } from './config'; +import { SidebarConfig, SidebarConfigContext } from './config'; import { MobileSidebarContext } from './MobileSidebar'; import { useSidebarPinState } from './SidebarPinStateContext'; @@ -100,6 +100,7 @@ const MobileSidebarGroup = (props: SidebarGroupProps) => { return ( // Material UI issue: https://github.com/mui-org/material-ui/issues/27820 <BottomNavigationAction + aria-label={label} label={label} icon={icon} component={Link as any} diff --git a/packages/core-components/src/layout/Sidebar/config.ts b/packages/core-components/src/layout/Sidebar/config.ts index dacf4bd0a5..e3d126a86f 100644 --- a/packages/core-components/src/layout/Sidebar/config.ts +++ b/packages/core-components/src/layout/Sidebar/config.ts @@ -32,7 +32,7 @@ export type SubmenuOptions = { drawerWidthOpen?: number; }; -/** @internal **/ +/** @internal */ export type SidebarConfig = { drawerWidthClosed: number; drawerWidthOpen: number; @@ -78,7 +78,7 @@ export const makeSidebarConfig = ( sidebarConfig.drawerWidthClosed - sidebarConfig.userBadgePadding * 2, }); -/** @internal **/ +/** @internal */ export type SubmenuConfig = { drawerWidthClosed: number; drawerWidthOpen: number; diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 88208e4430..e2da5bc048 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/core-plugin-api +## 1.4.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + +## 1.4.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + ## 1.3.0 ### Minor Changes diff --git a/packages/core-plugin-api/alpha-api-report.md b/packages/core-plugin-api/alpha-api-report.md new file mode 100644 index 0000000000..379e186df8 --- /dev/null +++ b/packages/core-plugin-api/alpha-api-report.md @@ -0,0 +1,26 @@ +## API Report File for "@backstage/core-plugin-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { ReactNode } from 'react'; + +// @alpha +export interface PluginOptionsProviderProps { + // (undocumented) + children: ReactNode; + // (undocumented) + plugin?: BackstagePlugin; +} + +// @alpha +export const PluginProvider: (props: PluginOptionsProviderProps) => JSX.Element; + +// @alpha +export function usePluginOptions< + TPluginOptions extends {} = {}, +>(): TPluginOptions; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 6bba47a3f4..035b837729 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -241,6 +241,11 @@ export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; +// @public +export const bitbucketServerAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + // @public export type BootErrorPageProps = { step: 'load-config' | 'load-chunk'; @@ -644,17 +649,6 @@ export type PluginFeatureFlagConfig = { name: string; }; -// @alpha -export interface PluginOptionsProviderProps { - // (undocumented) - children: ReactNode; - // (undocumented) - plugin?: BackstagePlugin; -} - -// @alpha -export const PluginProvider: (props: PluginOptionsProviderProps) => JSX.Element; - // @public export type ProfileInfo = { email?: string; @@ -757,11 +751,6 @@ export function useElementFilter<T>( dependencies?: any[], ): T; -// @alpha -export function usePluginOptions< - TPluginOptions extends {} = {}, ->(): TPluginOptions; - // @public export function useRouteRef<Optional extends boolean, Params extends AnyParams>( routeRef: ExternalRouteRef<Params, Optional>, diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 490ef25fbe..1da87fa457 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,12 +1,24 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "1.3.0", + "version": "1.4.1-next.0", "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "web-library" @@ -24,7 +36,7 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -57,10 +69,9 @@ "@types/prop-types": "^15.7.3", "@types/zen-observable": "^0.8.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ - "dist", - "alpha" + "dist" ] } diff --git a/packages/core-plugin-api/src/alpha.ts b/packages/core-plugin-api/src/alpha.ts new file mode 100644 index 0000000000..09983a48e9 --- /dev/null +++ b/packages/core-plugin-api/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './plugin-options'; diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 3ab8c98fee..21717e19e0 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -411,6 +411,21 @@ export const bitbucketAuthApiRef: ApiRef< id: 'core.auth.bitbucket', }); +/** + * Provides authentication towards Bitbucket Server APIs. + * + * @public + * @remarks + * + * See {@link https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html#BitbucketOAuth2.0providerAPI-scopes} + * for a full list of supported scopes. + */ +export const bitbucketServerAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.bitbucket-server', +}); + /** * Provides authentication towards Atlassian APIs. * diff --git a/packages/core-plugin-api/src/app/types.ts b/packages/core-plugin-api/src/app/types.ts index 2a71eda1f4..f3e0547ca4 100644 --- a/packages/core-plugin-api/src/app/types.ts +++ b/packages/core-plugin-api/src/app/types.ts @@ -17,7 +17,7 @@ // This is a bit of a hack that we use to avoid having to redeclare these types // within this package or have an explicit dependency on core-app-api. // These types end up being inlined and duplicated into this package at build time. -// eslint-disable-next-line no-restricted-imports +// eslint-disable-next-line @backstage/no-relative-monorepo-imports export type { BootErrorPageProps, SignInPageProps, diff --git a/packages/core-plugin-api/src/index.ts b/packages/core-plugin-api/src/index.ts index e8edd38ff3..30fc35b62a 100644 --- a/packages/core-plugin-api/src/index.ts +++ b/packages/core-plugin-api/src/index.ts @@ -22,7 +22,6 @@ export * from './analytics'; export * from './apis'; -export * from './plugin-options'; export * from './app'; export * from './extensions'; export * from './icons'; diff --git a/packages/core-plugin-api/src/plugin-options/usePluginOptions.tsx b/packages/core-plugin-api/src/plugin-options/usePluginOptions.tsx index 4fdacc1ace..8b3f7c109c 100644 --- a/packages/core-plugin-api/src/plugin-options/usePluginOptions.tsx +++ b/packages/core-plugin-api/src/plugin-options/usePluginOptions.tsx @@ -19,7 +19,7 @@ import { createVersionedValueMap, useVersionedContext, } from '@backstage/version-bridge'; -import { BackstagePlugin } from '../plugin'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; import React, { ReactNode } from 'react'; const contextKey: string = 'plugin-context'; diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 0d9df01182..149c474022 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/create-app +## 0.4.38-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## 0.4.37 + +### Patch Changes + +- 12a7b316ee: Bumped create-app version. +- 86a8dfd7b0: Added a check to ensure that Yarn v1 is used when creating new projects. +- 0eaa579f89: Update `SearchPage` template to use `SearchResult` extensions. +- 02f1316e57: Updated `packages/app/cypress/.eslintrc.json` to remove the unnecessary `import/no-extraneous-dependencies` rule. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## 0.4.37-next.2 + +### Patch Changes + +- 02f1316e57: Updated `packages/app/cypress/.eslintrc.json` to remove the unnecessary `import/no-extraneous-dependencies` rule. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## 0.4.37-next.1 + +### Patch Changes + +- 86a8dfd7b0: Added a check to ensure that Yarn v1 is used when creating new projects. +- 0eaa579f89: Update `SearchPage` template to use `SearchResult` extensions. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## 0.4.37-next.0 + +### Patch Changes + +- Bumped create-app version. + ## 0.4.36 ### Patch Changes diff --git a/packages/create-app/bin/backstage-create-app b/packages/create-app/bin/backstage-create-app index 60dfd4990a..4639ea96dc 100755 --- a/packages/create-app/bin/backstage-create-app +++ b/packages/create-app/bin/backstage-create-app @@ -25,7 +25,7 @@ if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) { require('..'); } else { // Only used for development, so should be a devDependency - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-undeclared-imports require('ts-node').register({ transpileOnly: true, project: path.resolve(__dirname, '../../../tsconfig.json'), diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 5d2e98992c..6ca80a519c 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.36", + "version": "0.4.38-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index 216fab6740..fa235e6175 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -22,6 +22,7 @@ import { program } from 'commander'; import { exitWithError } from './lib/errors'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { version } from '../../../package.json'; import createApp from './createApp'; diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 2894f1ffe9..95ced49490 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -193,27 +193,58 @@ describe('tasks', () => { // requires callback implementation to support `promisify` wrapper // https://stackoverflow.com/a/60579617/10044859 mockExec.mockImplementation((_command, callback) => { - callback(null, 'standard out', 'standard error'); + if (_command === 'yarn --version') { + callback(null, { stdout: '1.22.5', stderr: 'standard error' }); + } else { + callback(null, { stdout: 'standard out', stderr: 'standard error' }); + } }); const appDir = 'projects/dir'; await expect(buildAppTask(appDir)).resolves.not.toThrow(); - expect(mockChdir).toHaveBeenCalledTimes(2); + expect(mockChdir).toHaveBeenCalledTimes(1); expect(mockChdir).toHaveBeenNthCalledWith(1, appDir); - expect(mockChdir).toHaveBeenNthCalledWith(2, appDir); - expect(mockExec).toHaveBeenCalledTimes(2); + expect(mockExec).toHaveBeenCalledTimes(3); expect(mockExec).toHaveBeenNthCalledWith( 1, - 'yarn install', + 'yarn --version', expect.any(Function), ); expect(mockExec).toHaveBeenNthCalledWith( 2, + 'yarn install', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 3, 'yarn tsc', expect.any(Function), ); }); + it('should error out on incorrect yarn version', async () => { + const mockChdir = jest.spyOn(process, 'chdir'); + + // requires callback implementation to support `promisify` wrapper + // https://stackoverflow.com/a/60579617/10044859 + mockExec.mockImplementation((_command, callback) => { + callback(null, { stdout: '3.2.1', stderr: 'standard error' }); + }); + + const appDir = 'projects/dir'; + await expect(buildAppTask(appDir)).rejects.toThrow( + /^@backstage\/create-app requires Yarn v1, found '3\.2\.1'/, + ); + expect(mockChdir).toHaveBeenCalledTimes(1); + expect(mockChdir).toHaveBeenNthCalledWith(1, appDir); + expect(mockExec).toHaveBeenCalledTimes(1); + expect(mockExec).toHaveBeenNthCalledWith( + 1, + 'yarn --version', + expect.any(Function), + ); + }); + it('should fail if project directory does not exist', async () => { const appDir = 'projects/missingProject'; await expect(buildAppTask(appDir)).rejects.toThrow( diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 23017c8ec7..f158a2d1f1 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -199,9 +199,21 @@ export async function createTemporaryAppFolderTask(tempDir: string) { * @param appDir - location of application to build */ export async function buildAppTask(appDir: string) { + process.chdir(appDir); + + await Task.forItem('determining', 'yarn version', async () => { + const result = await exec('yarn --version'); + const yarnVersion = result.stdout?.trim(); + + if (yarnVersion && !yarnVersion.startsWith('1.')) { + throw new Error( + `@backstage/create-app requires Yarn v1, found '${yarnVersion}'. You can migrate the project to Yarn 3 after creation using https://backstage.io/docs/tutorials/yarn-migration`, + ); + } + }); + const runCmd = async (cmd: string) => { await Task.forItem('executing', cmd, async () => { - process.chdir(appDir); await exec(cmd).catch(error => { process.stdout.write(error.stderr); process.stdout.write(error.stdout); diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index b89b1d5463..72f5c2c9d5 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -/* eslint-disable monorepo/no-relative-import */ +/* eslint-disable @backstage/no-relative-monorepo-imports */ /* This is a list of all packages used by the template. If dependencies are added or removed, diff --git a/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json b/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json index 2b3a458b95..a467608916 100644 --- a/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json +++ b/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } 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 9f11d0c80c..1788dde1bd 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,5 +1,5 @@ import React from 'react'; -import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core'; +import { makeStyles, Theme, Grid, Paper } from '@material-ui/core'; import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; import { @@ -10,7 +10,6 @@ import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; import { SearchType } from '@backstage/plugin-search'; import { - DefaultResultListItem, SearchBar, SearchFilter, SearchResult, @@ -112,41 +111,8 @@ const SearchPage = () => { <Grid item xs={9}> <SearchPagination /> <SearchResult> - {({ results }) => ( - <List> - {results.map(({ type, document, highlight, rank }) => { - switch (type) { - case 'software-catalog': - return ( - <CatalogSearchResultListItem - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - case 'techdocs': - return ( - <TechDocsSearchResultListItem - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - default: - return ( - <DefaultResultListItem - key={document.location} - result={document} - highlight={highlight} - rank={rank} - /> - ); - } - })} - </List> - )} + <CatalogSearchResultListItem icon={<CatalogIcon />} /> + <TechDocsSearchResultListItem icon={<DocsIcon />} /> </SearchResult> </Grid> </Grid> diff --git a/packages/create-app/templates/default-app/packages/backend/README.md b/packages/create-app/templates/default-app/packages/backend/README.md index aa042db030..867487ba0a 100644 --- a/packages/create-app/templates/default-app/packages/backend/README.md +++ b/packages/create-app/templates/default-app/packages/backend/README.md @@ -56,4 +56,4 @@ and ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index febc77f339..ae7c5a3282 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,74 @@ # @backstage/dev-utils +## 1.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/test-utils@1.2.6-next.0 + - @backstage/app-defaults@1.2.1-next.0 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + +## 1.0.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/app-defaults@1.2.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/integration-react@1.1.10 + - @backstage/test-utils@1.2.5 + +## 1.0.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 1.0.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/app-defaults@1.1.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + +## 1.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/integration-react@1.1.9 + ## 1.0.11 ### Patch Changes diff --git a/packages/dev-utils/README.md b/packages/dev-utils/README.md index 78fa3bf7f6..9f71ef1b03 100644 --- a/packages/dev-utils/README.md +++ b/packages/dev-utils/README.md @@ -16,4 +16,4 @@ yarn add -D @backstage/dev-utils ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 110c086b5f..2680401145 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "1.0.11", + "version": "1.0.13-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/e2e-test/.eslintrc.js b/packages/e2e-test/.eslintrc.js index f9fb2e27c9..e2a53a6ad2 100644 --- a/packages/e2e-test/.eslintrc.js +++ b/packages/e2e-test/.eslintrc.js @@ -1,14 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - 'no-console': 0, - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: false, - peerDependencies: false, - bundledDependencies: false, - }, - ], - }, -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index d872d34664..c3345a5815 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,14 @@ # e2e-test +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.4.38-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + ## 0.2.0 ### Minor Changes diff --git a/packages/e2e-test/README.md b/packages/e2e-test/README.md index f3692de2a7..19f37a600e 100644 --- a/packages/e2e-test/README.md +++ b/packages/e2e-test/README.md @@ -21,4 +21,4 @@ If you make changes to other packages you will need to rerun `yarn tsc && yarn b ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 374eaa6d18..969c68feb3 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.2.0", + "version": "0.2.1-next.0", "private": true, "backstage": { "role": "cli" @@ -26,22 +26,25 @@ "clean": "backstage-cli package clean" }, "bin": "bin/e2e-test", - "devDependencies": { - "@backstage/cli": "workspace:^", + "dependencies": { "@backstage/cli-common": "workspace:^", + "@backstage/create-app": "workspace:^", "@backstage/errors": "workspace:^", - "@types/fs-extra": "^9.0.1", - "@types/node": "^16.11.26", - "@types/puppeteer": "^5.4.4", "chalk": "^4.0.0", "commander": "^9.1.0", "cross-fetch": "^3.1.5", "fs-extra": "10.1.0", "handlebars": "^4.7.3", - "nodemon": "^2.0.2", "pgtools": "^0.3.0", "puppeteer": "^17.0.0", - "tree-kill": "^1.2.2", + "tree-kill": "^1.2.2" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^9.0.1", + "@types/node": "^16.11.26", + "@types/puppeteer": "^5.4.4", + "nodemon": "^2.0.2", "ts-node": "^10.0.0" }, "nodemonConfig": { diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 3eedb90b21..b3fb8d9a3c 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -159,7 +159,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { appendDeps(pkg); } - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-forbidden-package-imports appendDeps(require('@backstage/create-app/package.json')); print(`Preparing workspace`); @@ -490,6 +490,17 @@ async function testBackendStart(appDir: string, ...args: string[]) { !l.includes('Update this package.json to use a subpath') && !l.includes( '(Use `node --trace-deprecation ...` to show where the warning was created)', + ) && + // These 4 are all for the AWS SDK v2 deprecation + !l.includes( + 'The AWS SDK for JavaScript (v2) will be put into maintenance mode', + ) && + !l.includes( + 'Please migrate your code to use AWS SDK for JavaScript', + ) && + !l.includes('check the migration guide at https://a.co/7PzMCcy') && + !l.includes( + '(Use `node --trace-warnings ...` to show where the warning was created)', ), ).length !== 0 ); diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md index 1a65ae2315..650a826a30 100644 --- a/packages/errors/api-report.md +++ b/packages/errors/api-report.md @@ -84,6 +84,9 @@ export class NotAllowedError extends CustomErrorBase {} // @public export class NotFoundError extends CustomErrorBase {} +// @public +export class NotImplementedError extends CustomErrorBase {} + // @public export class NotModifiedError extends CustomErrorBase {} diff --git a/packages/errors/src/errors/common.ts b/packages/errors/src/errors/common.ts index 80a3d84d82..e1a292d7b5 100644 --- a/packages/errors/src/errors/common.ts +++ b/packages/errors/src/errors/common.ts @@ -74,6 +74,13 @@ export class ConflictError extends CustomErrorBase {} */ export class NotModifiedError extends CustomErrorBase {} +/** + * The server does not support the functionality required to fulfill the request. + * + * @public + */ +export class NotImplementedError extends CustomErrorBase {} + /** * An error that forwards an underlying cause with additional context in the message. * diff --git a/packages/errors/src/errors/index.ts b/packages/errors/src/errors/index.ts index 9e1c2408af..efd3257fbf 100644 --- a/packages/errors/src/errors/index.ts +++ b/packages/errors/src/errors/index.ts @@ -24,6 +24,7 @@ export { NotAllowedError, NotFoundError, NotModifiedError, + NotImplementedError, } from './common'; export { CustomErrorBase } from './CustomErrorBase'; export { ResponseError } from './ResponseError'; diff --git a/packages/eslint-plugin/.eslintrc.js b/packages/eslint-plugin/.eslintrc.js new file mode 100644 index 0000000000..6b9de4544e --- /dev/null +++ b/packages/eslint-plugin/.eslintrc.js @@ -0,0 +1,34 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Custom lint config here since source is ES5 +module.exports = { + plugins: ['@backstage'], + env: { + node: true, + es2021: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + lib: require('@backstage/cli/config/tsconfig.json').compilerOptions.lib, + }, + rules: { + '@backstage/no-undeclared-imports': ['error'], + 'no-unused-expressions': 'off', + }, +}; diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md new file mode 100644 index 0000000000..7e5a3efbae --- /dev/null +++ b/packages/eslint-plugin/CHANGELOG.md @@ -0,0 +1,19 @@ +# @backstage/eslint-plugin + +## 0.1.2-next.0 + +### Patch Changes + +- a061c466d6: Fixing a bug that we should check internal dependencies too + +## 0.1.0 + +### Minor Changes + +- 179d301518: Added a new ESLint plugin with common rules for Backstage projects. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for more details. + +## 0.1.0-next.0 + +### Minor Changes + +- 179d301518: Added a new ESLint plugin with common rules for Backstage projects. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for more details. diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md new file mode 100644 index 0000000000..c311670603 --- /dev/null +++ b/packages/eslint-plugin/README.md @@ -0,0 +1,42 @@ +# @backstage/eslint-plugin + +A collection of ESLint rules useful to Backstage projects. + +## Usage + +This ESLint plugin is part of the default lint configuration provided by the [Backstage CLI](https://www.npmjs.com/package/@backstage/cli), so you generally do not need to install it manually. + +If you do wish to install this plugin manually, start by adding it as a development dependency to your project: + +```sh +yarn add --dev @backstage/eslint-plugin +``` + +Then add it to your ESLint configuration: + +```js +extends: [ + 'plugin:@backstage/recommended', +], +``` + +Alternatively, if you want to install in individual rules manually: + +```js +plugins: [ + '@backstage', +], +rules: { + '@backstage/no-forbidden-package-imports': 'error', +} +``` + +## Rules + +The following rules are provided by this plugin: + +| Rule | Description | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| [@backstage/no-forbidden-package-imports](./docs/rules/no-forbidden-package-imports.md) | Disallow internal monorepo imports from package subpaths that are not exported. | +| [@backstage/no-relative-monorepo-imports](./docs/rules/no-relative-monorepo-imports.md) | Forbid relative imports that reach outside of the package in a monorepo. | +| [@backstage/no-undeclared-imports](./docs/rules/no-undeclared-imports.md) | Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`. | diff --git a/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md b/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md new file mode 100644 index 0000000000..7a6ae11c37 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md @@ -0,0 +1,52 @@ +# @backstage/no-forbidden-package-imports + +Disallow internal monorepo imports from package subpaths that are not exported. + +## Usage + +Add the rules as follows, it has no options: + +```js +"@backstage/no-forbidden-package-imports": ["error"] +``` + +## Rule Details + +Given the following two target packages: + +```json +{ + "name": "@backstage/plugin-foo", + "files": ["dist", "type-utils"] +} +``` + +```json +{ + "name": "@backstage/plugin-bar", + "exports": { + ".": "./src/index.ts", + "./testUtils": "./src/testUtils/index.ts", + "./package.json": "./package.json" + } +} +``` + +### Fail + +```ts +import { FooCard } from '@backstage/plugin-foo/src/components'; +import { BarCard } from '@backstage/plugin-bar/src/components'; +``` + +### Pass + +```ts +import { FooCard } from '@backstage/plugin-foo'; +import { FooType } from '@backstage/plugin-foo/type-utils'; +import fooPkg from '@backstage/plugin-foo/package.json'; + +import { BarCard } from '@backstage/plugin-bar'; +import { renderBarCardExtension } from '@backstage/plugin-bar/testUtils'; +import barPkg from '@backstage/plugin-bar/package.json'; +``` diff --git a/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md b/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md new file mode 100644 index 0000000000..6a41a73fb6 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md @@ -0,0 +1,43 @@ +# @backstage/no-relative-monorepo-imports + +Forbid relative imports that reach outside of the package in a monorepo. + +## Usage + +Add the rules as follows, it has no options: + +```js +"@backstage/no-relative-monorepo-imports": ["error"] +``` + +The following patterns are considered files used during development, and only need dependencies to be declared in devDependencies: + +```python +!src/** # Any files outside of src are considered dev files +src/**/*.test.* +src/**/*.stories.* +src/**/__testUtils__/** +src/**/__mocks__/** +src/setupTests.* +``` + +## Rule Details + +Assuming an import from for example `plugins/bar/src/index.ts`: + +### Fail + +```ts +import { FooCard } from '../../foo'; + +import { FooCard } from '../../foo/src/components/FooCard'; +``` + +### Pass + +```ts +import { FooCard } from '@internal/plugin-foo'; + +// This is allowed by this rule, but not by no-forbidden-package-imports +import { FooCard } from '@internal/plugin-foo/src/components/FooCard'; +``` diff --git a/packages/eslint-plugin/docs/rules/no-undeclared-imports.md b/packages/eslint-plugin/docs/rules/no-undeclared-imports.md new file mode 100644 index 0000000000..6102c772e6 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-undeclared-imports.md @@ -0,0 +1,84 @@ +# @backstage/no-undeclared-imports + +Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`. + +## Usage + +Add the rules as follows, it has no options: + +```js +"@backstage/no-undeclared-imports": ["error"] +``` + +The following patterns are considered files used during development, and only need dependencies to be declared in devDependencies: + +```python +!src/** # Any files outside of src are considered dev files +src/**/*.test.* +src/**/*.stories.* +src/**/__testUtils__/** +src/**/__mocks__/** +src/setupTests.* +``` + +## Rule Details + +Given the following `package.json`: + +```json +{ + "name": "@backstage/plugin-foo", + "backstage": { + "role": "frontend-plugin" + }, + "dependencies": { + "react": "^17.0.0" + }, + "devDependencies": { + "@backstage/core-plugin-api": "^1.0.0" + }, + "peerDependencies": { + "@backstage/config": "*" + } +} +``` + +### Fail + +Inside `src/my-plugin.ts`: + +```ts +// Should be declared as a dependency +const _ = require('lodash'); +import _ from 'lodash'; + +// React should be a peer dependency in frontend plugins +import react from 'react'; + +// Should be declared as a dependency, not a dev dependency +import { useApi } from '@backstage/core-plugin-api'; +``` + +Inside `src/my-plugin.test.ts` (a test file): + +```ts +// Should be declared as a dev dependency +const _ = require('lodash'); +import _ from 'lodash'; +``` + +### Pass + +Inside `src/my-plugin.ts`: + +```ts +// Declared in peerDependencies, so it is allowed +import { ConfigReader } from '@backstage/config'; +``` + +Inside `src/my-plugin.test.ts` (a test file): + +```ts +// Declared as a dev dependency inside a test file +import { useApi } from '@backstage/core-plugin-api'; +``` diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js new file mode 100644 index 0000000000..20a0ff2d18 --- /dev/null +++ b/packages/eslint-plugin/index.js @@ -0,0 +1,33 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + configs: { + recommended: { + plugins: ['@backstage'], + rules: { + '@backstage/no-forbidden-package-imports': 'error', + '@backstage/no-relative-monorepo-imports': 'error', + '@backstage/no-undeclared-imports': 'error', + }, + }, + }, + rules: { + 'no-forbidden-package-imports': require('./rules/no-forbidden-package-imports'), + 'no-relative-monorepo-imports': require('./rules/no-relative-monorepo-imports'), + 'no-undeclared-imports': require('./rules/no-undeclared-imports'), + }, +}; diff --git a/packages/eslint-plugin/lib/getPackages.js b/packages/eslint-plugin/lib/getPackages.js new file mode 100644 index 0000000000..f5f2c63cc1 --- /dev/null +++ b/packages/eslint-plugin/lib/getPackages.js @@ -0,0 +1,71 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const path = require('path'); +const manypkg = require('@manypkg/get-packages'); + +/** + * @typedef ExtendedPackage + * @type {import('@manypkg/get-packages').Package & { packageJson: { exports?: Record<string, string>, files?: Array<string> }}} packageJson + */ + +/** + * @typedef PackageMap + * @type object + * + * @property {ExtendedPackage} root + * @property {ExtendedPackage[]} list + * @property {Map<string, ExtendedPackage>} map + * @property {(path: string) => ExtendedPackage | undefined} byPath + */ + +// Loads all packages in the monorepo once, and caches the result +module.exports = (function () { + /** @type {PackageMap | undefined} */ + let result = undefined; + /** @type {number} */ + let lastLoadAt = 0; + + /** @returns {PackageMap | undefined} */ + return function getPackages(/** @type {string} */ dir) { + if (result) { + // Only cache for 5 seconds, to avoid the need to reload ESLint servers + if (Date.now() - lastLoadAt > 5000) { + result = undefined; + } else { + return result; + } + } + const packages = manypkg.getPackagesSync(dir); + if (!packages) { + return undefined; + } + result = { + map: new Map(packages.packages.map(pkg => [pkg.packageJson.name, pkg])), + list: packages.packages, + root: packages.root, + byPath(filePath) { + return packages.packages.find( + pkg => !path.relative(pkg.dir, filePath).startsWith('..'), + ); + }, + }; + lastLoadAt = Date.now(); + return result; + }; +})(); diff --git a/packages/eslint-plugin/lib/visitImports.js b/packages/eslint-plugin/lib/visitImports.js new file mode 100644 index 0000000000..1a344c1101 --- /dev/null +++ b/packages/eslint-plugin/lib/visitImports.js @@ -0,0 +1,175 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const { builtinModules } = require('module'); +const getPackages = require('./getPackages'); + +/** + * @typedef LocalImport + * @type {object} + * @property {'local'} type + * @property {'value' | 'type'} kind + * @property {string} path + */ + +/** + * @typedef InternalImport + * @type {object} + * @property {'internal'} type + * @property {'value' | 'type'} kind + * @property {string} path + * @property {import('./getPackages').ExtendedPackage} package + * @property {string} packageName + */ + +/** + * @typedef ExternalImport + * @type {object} + * @property {'external'} type + * @property {'value' | 'type'} kind + * @property {string} path + * @property {string} packageName + */ + +/** + * @typedef BuiltinImport + * @type {object} + * @property {'builtin'} type + * @property {'value' | 'type'} kind + * @property {string} path + * @property {string} packageName + */ + +/** + * @callback ImportVisitor + * @param {ConsideredNode} node + * @param {LocalImport | InternalImport | ExternalImport | BuiltinImport} import + */ + +/** + * @typedef ConsideredNode + * @type {import('estree').ImportDeclaration | import('estree').ExportAllDeclaration | import('estree').ExportNamedDeclaration | import('estree').ImportExpression | import('estree').SimpleCallExpression} + */ + +/** + * @param {ConsideredNode} node + * @returns {undefined | {path: string, kind: 'type' | 'value'}} + */ +function getImportInfo(node) { + /** @type {import('estree').Expression | import('estree').SpreadElement | undefined | null} */ + let pathNode; + + if (node.type === 'CallExpression') { + if ( + node.callee.type === 'Identifier' && + node.callee.name == 'require' && + node.arguments.length === 1 + ) { + pathNode = node.arguments[0]; + } + } else { + pathNode = node.source; + } + + if (pathNode?.type !== 'Literal') { + return undefined; + } + if (typeof pathNode.value !== 'string') { + return undefined; + } + + /** @type {any} */ + const anyNode = node; + return { path: pathNode.value, kind: anyNode.importKind ?? 'value' }; +} + +/** + * @param visitor - Visitor callback + * @param {import('eslint').Rule.RuleContext} context + * @param {ImportVisitor} visitor + */ +module.exports = function visitImports(context, visitor) { + const packages = getPackages(context.getCwd()); + if (!packages) { + return; + } + + /** + * @param {ConsideredNode} node + */ + function visit(node) { + const info = getImportInfo(node); + if (!info) { + return; + } + + if (info.path[0] === '.') { + return visitor(node, { type: 'local', ...info }); + } + + const pathParts = info.path.split('/'); + + // Check for match with plain name, then namespaced name + let packageName; + let subPath; + if (info.path[0] === '@') { + packageName = pathParts.slice(0, 2).join('/'); + subPath = pathParts.slice(2).join('/'); + } else { + packageName = pathParts[0]; + subPath = pathParts.slice(1).join('/'); + } + const pkg = packages?.map.get(packageName); + if (!pkg) { + if ( + packageName.startsWith('node:') || + builtinModules.includes(packageName) + ) { + return visitor(node, { + type: 'builtin', + kind: info.kind, + path: subPath, + packageName, + }); + } + + return visitor(node, { + type: 'external', + kind: info.kind, + path: subPath, + packageName, + }); + } + + return visitor(node, { + type: 'internal', + kind: info.kind, + path: subPath, + package: pkg, + packageName, + }); + } + + return { + ImportDeclaration: visit, + ExportAllDeclaration: visit, + ExportNamedDeclaration: visit, + ImportExpression: visit, + CallExpression: visit, + }; +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json new file mode 100644 index 0000000000..fcc038cdbf --- /dev/null +++ b/packages/eslint-plugin/package.json @@ -0,0 +1,28 @@ +{ + "name": "@backstage/eslint-plugin", + "description": "Backstage ESLint plugin", + "version": "0.1.2-next.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/eslint-plugin" + }, + "license": "Apache-2.0", + "main": "./index.js", + "scripts": { + "lint": "backstage-cli package lint", + "test": "backstage-cli package test" + }, + "dependencies": { + "@manypkg/get-packages": "^1.1.3", + "minimatch": "^5.1.2" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "eslint": "^8.33.0" + } +} diff --git a/packages/eslint-plugin/rules/no-forbidden-package-imports.js b/packages/eslint-plugin/rules/no-forbidden-package-imports.js new file mode 100644 index 0000000000..e7c1acca27 --- /dev/null +++ b/packages/eslint-plugin/rules/no-forbidden-package-imports.js @@ -0,0 +1,74 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const visitImports = require('../lib/visitImports'); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + messages: { + forbidden: '{{packageName}} does not export {{subPath}}', + }, + docs: { + description: + 'Disallow internal monorepo imports from package subpaths that are not exported.', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md', + }, + }, + create(context) { + return visitImports(context, (node, imp) => { + if (imp.type !== 'internal') { + return; + } + // Empty subpaths are always allowed + if (!imp.path) { + return; + } + + // If the import is listed in the package.json exports field, we allow it + const exp = imp.package.packageJson.exports; + if (exp && (exp[imp.path] || exp['./' + imp.path])) { + return; + } + if (!exp) { + // If there's no exports field, we allow anything listed in files, except dist + const files = imp.package.packageJson.files; + if ( + !files || + files.some(f => !f.startsWith('dist') && imp.path.startsWith(f)) + ) { + return; + } + // And also package.json + if (imp.path === 'package.json') { + return; + } + } + + context.report({ + node: node, + messageId: 'forbidden', + data: { + packageName: imp.package.packageJson.name || imp.package.dir, + subPath: imp.path, + }, + }); + }); + }, +}; diff --git a/packages/eslint-plugin/rules/no-relative-monorepo-imports.js b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js new file mode 100644 index 0000000000..df30d35f67 --- /dev/null +++ b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js @@ -0,0 +1,85 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const path = require('path'); +const visitImports = require('../lib/visitImports'); +const getPackageMap = require('../lib/getPackages'); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + messages: { + outside: 'Import of {{path}} is outside of any known monorepo package', + forbidden: + "Relative imports of monorepo packages are forbidden, use '{{newImport}}' instead", + }, + docs: { + description: + 'Forbid relative imports that reach outside of the package in a monorepo.', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md', + }, + }, + create(context) { + const packages = getPackageMap(context.getCwd()); + if (!packages) { + return {}; + } + const filePath = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename(); + + const localPkg = packages.byPath(filePath); + if (!localPkg) { + return {}; + } + + return visitImports(context, (node, imp) => { + if (imp.type !== 'local') { + return; + } + + const target = path.resolve(path.dirname(filePath), imp.path); + if (!path.relative(localPkg.dir, target).startsWith('..')) { + return; + } + + const targetPkg = packages.byPath(target); + if (!targetPkg) { + context.report({ + node: node, + messageId: 'outside', + data: { + path: target, + }, + }); + return; + } + + const targetPath = path.relative(targetPkg.dir, target); + const targetName = targetPkg.packageJson.name ?? '<unknown>'; + context.report({ + node: node, + messageId: 'forbidden', + data: { + newImport: targetPath ? `${targetName}/${targetPath}` : targetName, + }, + }); + }); + }, +}; diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js new file mode 100644 index 0000000000..b1e11d1b97 --- /dev/null +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -0,0 +1,212 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const path = require('path'); +const getPackageMap = require('../lib/getPackages'); +const visitImports = require('../lib/visitImports'); +const minimatch = require('minimatch'); + +const depFields = { + dep: 'dependencies', + dev: 'devDependencies', + peer: 'peerDependencies', +}; + +const devModulePatterns = [ + new minimatch.Minimatch('!src/**'), + new minimatch.Minimatch('src/**/*.test.*'), + new minimatch.Minimatch('src/**/*.stories.*'), + new minimatch.Minimatch('src/**/__testUtils__/**'), + new minimatch.Minimatch('src/**/__mocks__/**'), + new minimatch.Minimatch('src/setupTests.*'), +]; + +function getExpectedDepType( + /** @type {any} */ localPkg, + /** @type {string} */ impPath, + /** @type {string} */ modulePath, +) { + const role = localPkg?.backstage?.role; + // Some package roles have known dependency types + switch (role) { + case 'common-library': + case 'web-library': + case 'frontend-plugin': + case 'frontend-plugin-module': + case 'node-library': + case 'backend-plugin': + case 'backend-plugin-module': + switch (impPath) { + case 'react': + case 'react-dom': + case 'react-router': + case 'react-router-dom': + return 'peer'; + } + break; + case 'cli': + case 'frontend': + case 'backend': + default: + break; + } + + for (const pattern of devModulePatterns) { + if (pattern.match(modulePath)) { + return 'dev'; + } + } + return 'dep'; +} + +/** + * + * @param {import('@manypkg/get-packages').Package['packageJson']} pkg + * @param {string} name + * @param {ReturnType<typeof getExpectedDepType>} expectedType + * @returns {{oldDepsField?: string, depsField: string} | undefined} + */ +function findConflict(pkg, name, expectedType) { + const isDep = pkg.dependencies?.[name]; + const isDevDep = pkg.devDependencies?.[name]; + const isPeerDep = pkg.peerDependencies?.[name]; + const depsField = depFields[expectedType]; + + if (expectedType === 'dep' && !isDep && !isPeerDep) { + const oldDepsField = isDevDep ? depFields.dev : undefined; + return { oldDepsField, depsField }; + } else if (expectedType === 'dev' && !isDevDep && !isDep && !isPeerDep) { + return { oldDepsField: undefined, depsField }; + } else if (expectedType === 'peer' && !isPeerDep) { + const oldDepsField = isDep + ? depFields.dep + : isDevDep + ? depFields.dev + : undefined; + + return { oldDepsField, depsField }; + } + return undefined; +} + +/** + * @param {string} depsField + */ +function getAddFlagForDepsField(depsField) { + switch (depsField) { + case depFields.dep: + return ''; + case depFields.dev: + return ' --dev'; + case depFields.peer: + return ' --peer'; + default: + return ''; + } +} + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + messages: { + undeclared: + "{{ packageName }} must be declared in {{ depsField }} of {{ packageJsonPath }}, run 'yarn --cwd {{ packagePath }} add{{ addFlag }} {{ packageName }}' from the project root.", + switch: + '{{ packageName }} is declared in {{ oldDepsField }}, but should be moved to {{ depsField }} in {{ packageJsonPath }}.', + }, + docs: { + description: + 'Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`.', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-undeclared-imports.md', + }, + }, + create(context) { + const packages = getPackageMap(context.getCwd()); + if (!packages) { + return {}; + } + const filePath = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename(); + + const localPkg = packages.byPath(filePath); + if (!localPkg) { + return {}; + } + + return visitImports(context, (node, imp) => { + // We leave checking of type imports to the repo-tools check, + // and we skip builtins and local imports + if ( + imp.kind === 'type' || + imp.type === 'builtin' || + imp.type === 'local' + ) { + return; + } + + // We skip imports for the package itself + if (imp.packageName === localPkg.packageJson.name) { + return; + } + + const modulePath = path.relative(localPkg.dir, filePath); + const expectedType = getExpectedDepType( + localPkg.packageJson, + imp.packageName, + modulePath, + ); + + const conflict = findConflict( + localPkg.packageJson, + imp.packageName, + expectedType, + ); + + if (conflict) { + try { + const fullImport = imp.path + ? `${imp.packageName}/${imp.path}` + : imp.packageName; + require.resolve(fullImport, { + paths: [localPkg.dir], + }); + } catch { + // If the dependency doesn't resolve then it's likely a type import, ignore + return; + } + + const packagePath = path.relative(packages.root.dir, localPkg.dir); + const packageJsonPath = path.join(packagePath, 'package.json'); + + context.report({ + node, + messageId: conflict.oldDepsField ? 'switch' : 'undeclared', + data: { + ...conflict, + packagePath, + addFlag: getAddFlagForDepsField(conflict.depsField), + packageName: imp.packageName, + packageJsonPath: packageJsonPath, + }, + }); + } + }); + }, +}; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/package.json new file mode 100644 index 0000000000..c35178c46f --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/package.json @@ -0,0 +1,6 @@ +{ + "name": "root", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json new file mode 100644 index 0000000000..efef27047b --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json @@ -0,0 +1,17 @@ +{ + "name": "@internal/bar", + "exports": { + ".": "./src/index.ts", + "./BarPage": "./src/components/Bar.tsx", + "./package.json": "./package.json" + }, + "backstage": { + "role": "frontend-plugin" + }, + "dependencies": { + "react-router": "*" + }, + "devDependencies": { + "react-router-dom": "*" + } +} diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json new file mode 100644 index 0000000000..563ec35120 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json @@ -0,0 +1,16 @@ +{ + "name": "@internal/foo", + "dependencies": { + "@internal/bar": "1.0.0" + }, + "devDependencies": { + "lodash": "*" + }, + "peerDependencies": { + "react": "*" + }, + "files": [ + "dist", + "type-utils" + ] +} diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/node_modules/@internal/foo/index.js b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/node_modules/@internal/foo/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/node_modules/@internal/foo/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/node_modules/@internal/foo/package.json new file mode 100644 index 0000000000..c15a2c1718 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/node_modules/@internal/foo/package.json @@ -0,0 +1,17 @@ +{ + "name": "@internal/foo", + "main": "index.js", + "dependencies": { + "@internal/bar": "1.0.0" + }, + "devDependencies": { + "lodash": "*" + }, + "peerDependencies": { + "react": "*" + }, + "files": [ + "dist", + "type-utils" + ] +} diff --git a/packages/eslint-plugin/src/no-forbidden-package-imports.test.ts b/packages/eslint-plugin/src/no-forbidden-package-imports.test.ts new file mode 100644 index 0000000000..349b71319f --- /dev/null +++ b/packages/eslint-plugin/src/no-forbidden-package-imports.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleTester } from 'eslint'; +import path from 'path'; +import rule from '../rules/no-forbidden-package-imports'; + +const RULE = 'no-forbidden-package-imports'; +const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo'); + +const ERR = (name: string, path: string) => ({ + message: `${name} does not export ${path}`, +}); + +process.chdir(FIXTURE); + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2021, + }, +}); + +ruleTester.run(RULE, rule, { + valid: [ + { + code: `import '@internal/foo'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/foo/type-utils'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/foo/type-utils/anything'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/foo/package.json'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/bar'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/bar/package.json'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/bar/BarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + ], + invalid: [ + { + code: `import '@internal/foo/FooPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'FooPage')], + }, + { + code: `import '@internal/foo/dist'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'dist')], + }, + { + code: `import '@internal/foo/dist/FooPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'dist/FooPage')], + }, + { + code: `import '@internal/foo/src/FooPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'src/FooPage')], + }, + { + code: `import '@internal/foo/src'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'src')], + }, + { + code: `import '@internal/bar/OtherBarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'OtherBarPage')], + }, + { + code: `import '@internal/bar/dist'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'dist')], + }, + { + code: `import '@internal/bar/dist/BarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'dist/BarPage')], + }, + { + code: `import '@internal/bar/src/BarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'src/BarPage')], + }, + { + code: `import '@internal/bar/src'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'src')], + }, + ], +}); diff --git a/packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts b/packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts new file mode 100644 index 0000000000..6888d258cc --- /dev/null +++ b/packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleTester } from 'eslint'; +import path from 'path'; +import rule from '../rules/no-relative-monorepo-imports'; + +const RULE = 'no-relative-monorepo-imports'; +const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo'); + +const ERR_OUTSIDE = (path: string) => ({ + message: `Import of ${path} is outside of any known monorepo package`, +}); +const ERR_FORBIDDEN = (newImp: string) => ({ + message: `Relative imports of monorepo packages are forbidden, use '${newImp}' instead`, +}); + +process.chdir(FIXTURE); + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2021, + }, +}); + +ruleTester.run(RULE, rule, { + valid: [ + { + code: `import { version } from '@internal/foo'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + }, + { + code: `import { version } from '@internal/foo/src'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + }, + ], + invalid: [ + { + code: `import { version } from '../../foo'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_FORBIDDEN('@internal/foo')], + }, + { + code: `import { version } from '../../foo/src'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_FORBIDDEN('@internal/foo/src')], + }, + { + code: `import { version } from '../../../package.json'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_OUTSIDE(path.join(FIXTURE, 'package.json'))], + }, + ], +}); diff --git a/packages/eslint-plugin/src/no-undeclared-imports.test.ts b/packages/eslint-plugin/src/no-undeclared-imports.test.ts new file mode 100644 index 0000000000..78844899ba --- /dev/null +++ b/packages/eslint-plugin/src/no-undeclared-imports.test.ts @@ -0,0 +1,251 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleTester } from 'eslint'; +import { join as joinPath } from 'path'; +import rule from '../rules/no-undeclared-imports'; + +const RULE = 'no-undeclared-imports'; +const FIXTURE = joinPath(__dirname, '__fixtures__/monorepo'); + +const ERR_UNDECLARED = ( + name: string, + field: string, + path: string, + flag?: string, +) => ({ + message: `${name} must be declared in ${field} of ${joinPath( + path, + 'package.json', + )}, run 'yarn --cwd ${path} add${ + flag ? ` ${flag}` : '' + } ${name}' from the project root.`, +}); +const ERR_SWITCHED = ( + name: string, + old: string, + field: string, + path: string, +) => ({ + message: `${name} is declared in ${old}, but should be moved to ${field} in ${joinPath( + path, + 'package.json', + )}.`, +}); + +process.chdir(FIXTURE); + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2021, + }, +}); + +ruleTester.run(RULE, rule, { + valid: [ + { + code: `import '@internal/foo'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + }, + { + code: `import '@internal/bar'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + }, + { + code: `import 'react'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + }, + { + code: `import '@internal/foo'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + code: `import '@internal/bar'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + code: `import 'react'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + // We're only able to validate literals + code: `require('lod' + 'ash')`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + }, + ], + invalid: [ + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + errors: [ + ERR_SWITCHED( + 'lodash', + 'devDependencies', + 'dependencies', + joinPath('packages', 'foo'), + ), + ], + }, + { + code: `import 'react-router'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_SWITCHED( + 'react-router', + 'dependencies', + 'peerDependencies', + joinPath('packages', 'bar'), + ), + ], + }, + { + code: `import 'react-router-dom'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_SWITCHED( + 'react-router-dom', + 'devDependencies', + 'peerDependencies', + joinPath('packages', 'bar'), + ), + ], + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import { debounce } from 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import * as _ from 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import _ from 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import('lodash')`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `require('lodash')`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.test.ts'), + errors: [ + ERR_UNDECLARED( + 'lodash', + 'devDependencies', + joinPath('packages', 'bar'), + '--dev', + ), + ], + }, + { + code: `import 'react'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED( + 'react', + 'peerDependencies', + joinPath('packages', 'bar'), + '--peer', + ), + ], + }, + { + code: `import 'react'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.test.ts'), + errors: [ + ERR_UNDECLARED( + 'react', + 'peerDependencies', + joinPath('packages', 'bar'), + '--peer', + ), + ], + }, + { + code: `import 'react-dom'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + errors: [ + ERR_UNDECLARED( + 'react-dom', + 'dependencies', + joinPath('packages', 'foo'), + ), + ], + }, + { + code: `import 'react-dom'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + errors: [ + ERR_UNDECLARED( + 'react-dom', + 'devDependencies', + joinPath('packages', 'foo'), + '--dev', + ), + ], + }, + { + code: `import '@internal/foo'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED( + '@internal/foo', + 'dependencies', + joinPath('packages', 'bar'), + ), + ], + }, + ], +}); diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 27ee4dd712..700b410bf6 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/integration-react +## 1.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## 1.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + +## 1.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## 1.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + ## 1.1.9 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index d0eb4e8620..ab163f9b1a 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "1.1.9", + "version": "1.1.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,7 +45,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/packages/integration/package.json b/packages/integration/package.json index 90f82dae35..3ad53b1e08 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -46,7 +46,7 @@ "@backstage/config-loader": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/luxon": "^3.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/packages/release-manifests/package.json b/packages/release-manifests/package.json index 4abeb9dc6a..96c5aec484 100644 --- a/packages/release-manifests/package.json +++ b/packages/release-manifests/package.json @@ -38,7 +38,7 @@ "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/node": "^16.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 5969521581..d4d300f450 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/repo-tools +## 0.1.3-next.0 + +### Patch Changes + +- 32a4a05838: Tweaked type dependency check to trim wildcard type imports. +- 6ba8faf22a: The API report generation process is now able to detect and generate reports for additional entry points declared in the package `"exports"` field. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + +## 0.1.2 + +### Patch Changes + +- ff63acf30a: Packages without a declared `backstage.role` are now ignored. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + +## 0.1.2-next.0 + +### Patch Changes + +- ff63acf30a: Packages without a declared `backstage.role` are now ignored. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + ## 0.1.1 ### Patch Changes diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index 3378d20ea7..0e1b2d2a4c 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -29,6 +29,7 @@ Options: -a, --allow-warnings <allowWarningsPaths> --allow-all-warnings -o, --omit-messages <messageCodes> + --validate-release-tags -h, --help ``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index dd70575433..2ceeeb0dd8 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/repo-tools", "description": "CLI for Backstage repo tooling ", - "version": "0.1.1", + "version": "0.1.3-next.0", "publishConfig": { "access": "public" }, @@ -45,6 +45,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@backstage/types": "workspace:^", "@types/is-glob": "^4.0.2", "@types/mock-fs": "^4.13.0", "mock-fs": "^5.1.0" diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index b037f40be6..1fe7602617 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -62,6 +62,7 @@ import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/ import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; import { paths as cliPaths } from '../../lib/paths'; import minimatch from 'minimatch'; +import { getPackageExportNames } from '../../lib/entryPoints'; const tmpDir = cliPaths.resolveTargetRoot( './node_modules/.cache/api-extractor', @@ -237,10 +238,9 @@ export async function createTemporaryTsConfig(includedPackageDirs: string[]) { return path; } -export async function countApiReportWarnings(projectFolder: string) { - const path = resolvePath(projectFolder, 'api-report.md'); +export async function countApiReportWarnings(reportPath: string) { try { - const content = await fs.readFile(path, 'utf8'); + const content = await fs.readFile(reportPath, 'utf8'); const lines = content.split('\n'); const lineWarnings = lines.filter(line => @@ -300,6 +300,32 @@ function logApiReportInstructions() { console.log(''); } +async function findPackageEntryPoints(packageDirs: string[]): Promise< + Array<{ + packageDir: string; + name: string; + usesExperimentalTypeBuild?: boolean; + }> +> { + return Promise.all( + packageDirs.map(async packageDir => { + const pkg = await fs.readJson( + cliPaths.resolveTargetRoot(packageDir, 'package.json'), + ); + + return ( + getPackageExportNames(pkg)?.map(name => ({ packageDir, name })) ?? { + packageDir, + name: 'index', + usesExperimentalTypeBuild: pkg.scripts?.build?.includes( + '--experimental-type-build', + ), + } + ); + }), + ).then(results => results.flat()); +} + interface ApiExtractionOptions { packageDirs: string[]; outputDir: string; @@ -307,6 +333,7 @@ interface ApiExtractionOptions { tsconfigFilePath: string; allowWarnings?: boolean | string[]; omitMessages?: string[]; + validateReleaseTags?: boolean; } export async function runApiExtraction({ @@ -316,12 +343,15 @@ export async function runApiExtraction({ tsconfigFilePath, allowWarnings = false, omitMessages = [], + validateReleaseTags = false, }: ApiExtractionOptions) { await fs.remove(outputDir); - const entryPoints = packageDirs.map(packageDir => { + const packageEntryPoints = await findPackageEntryPoints(packageDirs); + + const entryPoints = packageEntryPoints.map(({ packageDir, name }) => { return cliPaths.resolveTargetRoot( - `./dist-types/${packageDir}/src/index.d.ts`, + `./dist-types/${packageDir}/src/${name}.d.ts`, ); }); @@ -337,7 +367,11 @@ export async function runApiExtraction({ } const warnings = new Array<string>(); - for (const packageDir of packageDirs) { + for (const { + packageDir, + name, + usesExperimentalTypeBuild, + } of packageEntryPoints) { console.log(`## Processing ${packageDir}`); const noBail = Array.isArray(allowWarnings) ? allowWarnings.some(aw => aw === packageDir || minimatch(packageDir, aw)) @@ -349,11 +383,15 @@ export async function runApiExtraction({ packageDir, ); - const warningCountBefore = await countApiReportWarnings(projectFolder); + const prefix = name === 'index' ? '' : `${name}-`; + const reportFileName = `${prefix}api-report.md`; + const reportPath = resolvePath(projectFolder, reportFileName); + + const warningCountBefore = await countApiReportWarnings(reportPath); const extractorConfig = ExtractorConfig.prepare({ configObject: { - mainEntryPointFilePath: resolvePath(packageFolder, 'src/index.d.ts'), + mainEntryPointFilePath: resolvePath(packageFolder, `src/${name}.d.ts`), bundledPackages: [], compiler: { @@ -362,16 +400,21 @@ export async function runApiExtraction({ apiReport: { enabled: true, - reportFileName: 'api-report.md', + reportFileName, reportFolder: projectFolder, - reportTempFolder: resolvePath(outputDir, '<unscopedPackageName>'), + reportTempFolder: resolvePath( + outputDir, + `${prefix}<unscopedPackageName>`, + ), }, docModel: { - enabled: true, + // TODO(Rugvip): This skips docs for non-index entry points. We can try to work around it, but + // most likely it makes sense to wait for API Extractor to natively support exports. + enabled: name === 'index', apiJsonFilePath: resolvePath( outputDir, - '<unscopedPackageName>.api.json', + `${prefix}<unscopedPackageName>.api.json`, ), }, @@ -461,6 +504,35 @@ export async function runApiExtraction({ compilerState, }); + // This release tag validation makes sure that the release tag of known entry points match expectations. + // The root index entrypoint is only allowed @public exports, while /alpha and /beta only allow @alpha and @beta. + if (validateReleaseTags && !usesExperimentalTypeBuild) { + if (['index', 'alpha', 'beta'].includes(name)) { + const report = await fs.readFile( + extractorConfig.reportFilePath, + 'utf8', + ); + const lines = report.split(/\r?\n/); + const expectedTag = name === 'index' ? 'public' : name; + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + const match = line.match(/^\/\/ @(alpha|beta|public)/); + if (match && match[1] !== expectedTag) { + // Because of limitations in the type script rollup logic we need to allow public exports from the other release stages + // TODO(Rugvip): Try to work around the need for this exception + if (expectedTag !== 'public' && match[1] === 'public') { + continue; + } + throw new Error( + `Unexpected release tag ${match[1]} in ${ + extractorConfig.reportFilePath + } at line ${i + 1}`, + ); + } + } + } + } + if (!extractorResult.succeeded) { if (shouldLogInstructions) { logApiReportInstructions(); @@ -488,7 +560,7 @@ export async function runApiExtraction({ ); } - const warningCountAfter = await countApiReportWarnings(projectFolder); + const warningCountAfter = await countApiReportWarnings(reportPath); if (noBail) { console.log(`Skipping warnings check for ${packageDir}`); } @@ -1107,7 +1179,7 @@ export async function categorizePackageDirs(packageDirs: any[]) { }); const role = pkgJson?.backstage?.role; if (!role) { - throw new Error(`No backstage.role in ${dir}/package.json`); + return; // Ignore packages without roles } if (role === 'cli') { cliPackageDirs.push(dir); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.ts b/packages/repo-tools/src/commands/api-reports/api-reports.ts index 66436644d2..a917975750 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports.ts @@ -33,6 +33,7 @@ type Options = { allowWarnings?: string; allowAllWarnings?: boolean; omitMessages?: string; + validateReleaseTags?: boolean; } & OptionValues; export const buildApiReports = async (paths: string[] = [], opts: Options) => { @@ -90,6 +91,7 @@ export const buildApiReports = async (paths: string[] = [], opts: Options) => { tsconfigFilePath, allowWarnings: allowAllWarnings || allowWarnings, omitMessages: Array.isArray(omitMessages) ? omitMessages : [], + validateReleaseTags: opts.validateReleaseTags, }); } if (cliPackageDirs.length > 0) { diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index f7a16b6d4e..f283e9c743 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -37,6 +37,10 @@ export function registerCommands(program: Command) { '-o, --omit-messages <messageCodes>', 'select some message code to be omited on the API Extractor (comma separated values i.e ae-cyclic-inherit-doc,ae-missing-getter )', ) + .option( + '--validate-release-tags', + 'Turn on release tag validation for the public, beta, and alpha APIs', + ) .description('Generate an API report for selected packages') .action( lazy(() => diff --git a/packages/repo-tools/src/commands/type-deps/type-deps.ts b/packages/repo-tools/src/commands/type-deps/type-deps.ts index d4dd28aa14..17164cea39 100644 --- a/packages/repo-tools/src/commands/type-deps/type-deps.ts +++ b/packages/repo-tools/src/commands/type-deps/type-deps.ts @@ -17,9 +17,10 @@ import fs from 'fs'; import { resolve as resolvePath } from 'path'; // Cba polluting root package.json, we'll have this -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import chalk from 'chalk'; import { getPackages, Package } from '@manypkg/get-packages'; +import { getPackageExportNames } from '../../lib/entryPoints'; export default async () => { const { packages } = await getPackages(resolvePath('.')); @@ -96,16 +97,23 @@ function findAllDeps(declSrc: string) { * missing or incorrect in package.json */ function checkTypes(pkg: Package) { - const typeDecl = fs.readFileSync( - resolvePath(pkg.dir, 'dist/index.d.ts'), - 'utf8', - ); - const allDeps = findAllDeps(typeDecl); + const entryPointNames = getPackageExportNames(pkg.packageJson) ?? ['index']; + + const allDeps = entryPointNames.flatMap(name => { + const typeDecl = fs.readFileSync( + resolvePath(pkg.dir, `dist/${name}.d.ts`), + 'utf8', + ); + return findAllDeps(typeDecl); + }); const deps = Array.from(new Set(allDeps)); const errors = []; const typeDeps = []; - for (const dep of deps) { + for (let dep of deps) { + if (dep.endsWith('/*')) { + dep = dep.slice(0, -2); + } try { const typeDep = findTypesPackage(dep, pkg); if (typeDep) { diff --git a/packages/repo-tools/src/lib/entryPoints.ts b/packages/repo-tools/src/lib/entryPoints.ts new file mode 100644 index 0000000000..4a412d410b --- /dev/null +++ b/packages/repo-tools/src/lib/entryPoints.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { extname } from 'path'; +import type { JsonObject } from '@backstage/types'; + +export function getPackageExportNames(pkg: JsonObject): string[] | undefined { + if (pkg.exports && typeof pkg.exports !== 'string') { + return Object.entries(pkg.exports).flatMap(([mount, path]) => { + const ext = extname(String(path)); + if (!['.ts', '.tsx', '.cts', '.mts'].includes(ext)) { + return []; // Ignore non-TS entry points + } + let name = mount; + if (name.startsWith('./')) { + name = name.slice(2); + } + if (!name || name === '.') { + return ['index']; + } + return [name]; + }); + } + + return undefined; +} diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index d87c99bd02..e3b49ff2f2 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,93 @@ # techdocs-cli-embedded-app +## 0.2.80-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.4-next.0 + - @backstage/plugin-techdocs@1.6.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/test-utils@1.2.6-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + - @backstage/app-defaults@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + +## 0.2.79 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/cli@0.22.2 + - @backstage/plugin-techdocs@1.5.0 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/app-defaults@1.2.0 + - @backstage/config@1.0.6 + - @backstage/integration-react@1.1.10 + - @backstage/test-utils@1.2.5 + - @backstage/plugin-techdocs-react@1.1.3 + +## 0.2.79-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## 0.2.79-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.0 + - @backstage/plugin-techdocs@1.5.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/app-defaults@1.1.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + +## 0.2.79-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.4-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/cli@0.22.1 + - @backstage/integration-react@1.1.9 + ## 0.2.78 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json b/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json index 2b3a458b95..a467608916 100644 --- a/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json +++ b/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index d071574b32..2d8c47c1f0 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.78", + "version": "0.2.80-next.0", "private": true, "backstage": { "role": "frontend" @@ -20,7 +20,7 @@ "@backstage/plugin-techdocs-react": "workspace:^", "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.11.0", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "history": "^5.0.0", "react": "^17.0.2", diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 463386bfc0..1e995b5f23 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,71 @@ # @techdocs/cli +## 1.3.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/plugin-techdocs-node@1.5.1-next.0 + +## 1.3.2 + +### Patch Changes + +- dd1e37649f: Deprecated getMkDocsYml in favor of getMkdocsYml (lowercase 'd') +- dcacf94912: Fix proxying to mkdocs + + The domain localhost may point to both 127.0.0.1 and ::1, ipv4 and ipv6 + and when node tries to lookup localhost it might prefer ipv6 while mkdocs + is only listening on ipv4. This tells node-proxy to target the ipv4 address + instead of relying on localhost hostname lookup. + +- 339d9a5b5c: Added support for using a default `mkdocs.yml` configuration file when none is provided +- 6e0b6a0d50: Fixed publish command missing awsBucketRootPath option. + Fixed publish command having the gcsBucketRootPath option misconfigured, previously returning a boolean vs a string. +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-techdocs-node@1.5.0 + - @backstage/catalog-model@1.2.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + +## 1.3.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/plugin-techdocs-node@1.4.6-next.2 + +## 1.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/plugin-techdocs-node@1.4.6-next.1 + +## 1.3.2-next.0 + +### Patch Changes + +- 6e0b6a0d50: Fixed publish command missing awsBucketRootPath option. + Fixed publish command having the gcsBucketRootPath option misconfigured, previously returning a boolean vs a string. +- Updated dependencies + - @backstage/plugin-techdocs-node@1.4.6-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + ## 1.3.0 ### Minor Changes diff --git a/packages/techdocs-cli/cli-report.md b/packages/techdocs-cli/cli-report.md index 9230fe9f1c..556068de85 100644 --- a/packages/techdocs-cli/cli-report.md +++ b/packages/techdocs-cli/cli-report.md @@ -33,6 +33,7 @@ Options: --no-docker --techdocs-ref <HOST_TYPE:URL> --etag <ETAG> + --site-name -v --verbose --omitTechdocsCoreMkdocsPlugin --legacyCopyReadmeMdToIndexMd @@ -52,7 +53,6 @@ Options: --awsRoleArn <AWS ROLE ARN> --awsEndpoint <AWS ENDPOINT> --awsS3ForcePathStyle - --awsBucketRootPath --osCredentialId <OPENSTACK SWIFT APPLICATION CREDENTIAL ID> --osSecret <OPENSTACK SWIFT APPLICATION CREDENTIAL SECRET> --osAuthUrl <OPENSTACK SWIFT AUTHURL> @@ -77,13 +77,15 @@ Options: --azureAccountKey <AZURE ACCOUNT KEY> --awsRoleArn <AWS ROLE ARN> --awsEndpoint <AWS ENDPOINT> + --awsProxy <HTTPS Proxy> --awsS3sse <AWS SSE> --awsS3ForcePathStyle + --awsBucketRootPath <AWS BUCKET ROOT PATH> --osCredentialId <OPENSTACK SWIFT APPLICATION CREDENTIAL ID> --osSecret <OPENSTACK SWIFT APPLICATION CREDENTIAL SECRET> --osAuthUrl <OPENSTACK SWIFT AUTHURL> --osSwiftUrl <OPENSTACK SWIFT SWIFTURL> - --gcsBucketRootPath + --gcsBucketRootPath <GCS BUCKET ROOT PATH> --directory <PATH> -h, --help ``` @@ -98,6 +100,7 @@ Options: --docker-entrypoint <DOCKER_ENTRYPOINT> --docker-option <DOCKER_OPTION...> --no-docker + --site-name --mkdocs-port <PORT> -v --verbose --preview-app-bundle-path <PATH_TO_BUNDLE> @@ -115,6 +118,7 @@ Options: --docker-entrypoint <DOCKER_ENTRYPOINT> --docker-option <DOCKER_OPTION...> --no-docker + --site-name -p, --port <PORT> -v --verbose -h, --help diff --git a/packages/techdocs-cli/cypress.config.js b/packages/techdocs-cli/cypress.config.js new file mode 100644 index 0000000000..abaa324d76 --- /dev/null +++ b/packages/techdocs-cli/cypress.config.js @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { defineConfig } from 'cypress'; +import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/plugins'; + +module.exports = defineConfig({ + e2e: { + setupNodeEvents(on, config) { + initPlugin(on, config); + }, + + excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'], + }, + viewportWidth: 1920, + viewportHeight: 1080, + includeShadowDom: true, +}); diff --git a/packages/techdocs-cli/cypress.json b/packages/techdocs-cli/cypress.json deleted file mode 100644 index b6f3b96019..0000000000 --- a/packages/techdocs-cli/cypress.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "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 index 2481ac5715..06965a82da 100644 --- a/packages/techdocs-cli/cypress/.eslintrc.json +++ b/packages/techdocs-cli/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "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/integration/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png b/packages/techdocs-cli/cypress/e2e/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png similarity index 100% rename from packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png rename to packages/techdocs-cli/cypress/e2e/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png 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/e2e/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png similarity index 100% rename from packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png rename to packages/techdocs-cli/cypress/e2e/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png diff --git a/packages/techdocs-cli/cypress/integration/backstage_serve.js b/packages/techdocs-cli/cypress/e2e/backstage_serve.cy.js similarity index 94% rename from packages/techdocs-cli/cypress/integration/backstage_serve.js rename to packages/techdocs-cli/cypress/e2e/backstage_serve.cy.js index 15fc03f8f4..0267c8bfe6 100644 --- a/packages/techdocs-cli/cypress/integration/backstage_serve.js +++ b/packages/techdocs-cli/cypress/e2e/backstage_serve.cy.js @@ -48,11 +48,11 @@ describe('TechDocs Live Preview - Backstage Serve', () => { cy.get('.md-footer').should('have.length', 1); }); - it('toMatchImageSnapshot - Backstage TechDocs Page', () => { + it('matchImage - Backstage TechDocs Page', () => { cy.visit( `${Cypress.env('backstageBaseUrl')}/docs/default/component/local`, ).then(() => { - cy.document().toMatchImageSnapshot(); + cy.document().matchImage(); }); }); }); diff --git a/packages/techdocs-cli/cypress/integration/mkdocs_serve.js b/packages/techdocs-cli/cypress/e2e/mkdocs_serve.cy.js similarity index 88% rename from packages/techdocs-cli/cypress/integration/mkdocs_serve.js rename to packages/techdocs-cli/cypress/e2e/mkdocs_serve.cy.js index 1ace7298ce..d5417e615f 100644 --- a/packages/techdocs-cli/cypress/integration/mkdocs_serve.js +++ b/packages/techdocs-cli/cypress/e2e/mkdocs_serve.cy.js @@ -16,7 +16,7 @@ /// <reference types="cypress" /> describe('TechDocs Live Preview - MkDocs Serve', () => { it('successfully serves documentation', () => { - cy.visit(Cypress.env('mkDocsBaseUrl')); + cy.visit(Cypress.env('mkdocsBaseUrl')); cy.contains('hello mock docs'); }); @@ -40,9 +40,9 @@ describe('TechDocs Live Preview - MkDocs Serve', () => { cy.get('.md-footer').should('have.length', 1); }); - it('toMatchImageSnapshot - MkDocs Page', () => { - cy.visit(Cypress.env('mkDocsBaseUrl')).then(() => { - cy.document().toMatchImageSnapshot(); + it('matchImage - MkDocs Page', () => { + cy.visit(Cypress.env('mkdocsBaseUrl')).then(() => { + cy.document().matchImage(); }); }); }); diff --git a/packages/techdocs-cli/cypress/plugins/index.js b/packages/techdocs-cli/cypress/plugins/index.js deleted file mode 100644 index 92faabb6ad..0000000000 --- a/packages/techdocs-cli/cypress/plugins/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// *********************************************************** -// 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/support/index.js b/packages/techdocs-cli/cypress/support/e2e.js similarity index 94% rename from packages/techdocs-cli/cypress/support/index.js rename to packages/techdocs-cli/cypress/support/e2e.js index a1a5b10ea4..f06a08e9a5 100644 --- a/packages/techdocs-cli/cypress/support/index.js +++ b/packages/techdocs-cli/cypress/support/e2e.js @@ -30,7 +30,7 @@ // Import commands.js using ES2015 syntax: import './commands'; -import 'cypress-plugin-snapshots/commands'; +import '@frsource/cypress-plugin-visual-regression-diff/commands'; // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index a538ee2c4e..0a3bbed06f 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.3.0", + "version": "1.3.3-next.0", "publishConfig": { "access": "public" }, @@ -37,6 +37,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@frsource/cypress-plugin-visual-regression-diff": "^3.2.8", "@types/commander": "^2.12.2", "@types/fs-extra": "^9.0.6", "@types/http-proxy": "^1.17.4", @@ -44,7 +45,6 @@ "@types/serve-handler": "^6.1.0", "@types/webpack-env": "^1.15.3", "cypress": "^10.0.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", diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index 874b904d86..5d80beb38a 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -21,6 +21,7 @@ import Docker from 'dockerode'; import { TechdocsGenerator, ParsedLocationAnnotation, + getMkdocsYml, } from '@backstage/plugin-techdocs-node'; import { ContainerRunner, @@ -30,8 +31,8 @@ import { ConfigReader } from '@backstage/config'; import { convertTechDocsRefToLocationAnnotation, createLogger, + getLogStream, } from '../../lib/utility'; -import { stdout } from 'process'; export default async function generate(opts: OptionValues) { // Use techdocs-node package to generate docs. Keep consistency between Backstage and CI generating docs. @@ -54,6 +55,10 @@ export default async function generate(opts: OptionValues) { await fs.ensureDir(outputDir); + const { path: mkdocsYmlPath, configIsTemporary } = await getMkdocsYml( + sourceDir, + ); + const config = new ConfigReader({ techdocs: { generator: { @@ -105,8 +110,15 @@ export default async function generate(opts: OptionValues) { : {}), logger, etag: opts.etag, - ...(process.env.LOG_LEVEL === 'debug' ? { logStream: stdout } : {}), + logStream: getLogStream(logger), + siteOptions: { name: opts.siteName }, }); + if (configIsTemporary) { + process.on('exit', async () => { + fs.rmSync(mkdocsYmlPath, {}); + }); + } + logger.info('Done!'); } diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index d4303ad306..98816a6b32 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -54,6 +54,11 @@ export function registerCommands(program: Command) { '--etag <ETAG>', 'A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json.', ) + .option( + '--site-name', + 'Name for site when using default MkDocs config', + 'Documentation Site', + ) .option('-v --verbose', 'Enable verbose output.', false) .option( '--omitTechdocsCoreMkdocsPlugin', @@ -101,10 +106,6 @@ export function registerCommands(program: Command) { '--awsS3ForcePathStyle', 'Optional AWS S3 option to force path style.', ) - .option( - '--awsBucketRootPath', - 'Optional sub-directory to store files in Amazon S3', - ) .option( '--osCredentialId <OPENSTACK SWIFT APPLICATION CREDENTIAL ID>', '(Required for OpenStack) specify when --publisher-type openStackSwift', @@ -172,11 +173,19 @@ export function registerCommands(program: Command) { '--awsEndpoint <AWS ENDPOINT>', 'Optional AWS endpoint to send requests to.', ) + .option( + '--awsProxy <HTTPS Proxy>', + 'Optional Proxy to use for AWS requests.', + ) .option('--awsS3sse <AWS SSE>', 'Optional AWS S3 Server Side Encryption.') .option( '--awsS3ForcePathStyle', 'Optional AWS S3 option to force path style.', ) + .option( + '--awsBucketRootPath <AWS BUCKET ROOT PATH>', + 'Optional sub-directory to store files in Amazon S3', + ) .option( '--osCredentialId <OPENSTACK SWIFT APPLICATION CREDENTIAL ID>', '(Required for OpenStack) specify when --publisher-type openStackSwift', @@ -194,7 +203,7 @@ export function registerCommands(program: Command) { '(Required for OpenStack) specify when --publisher-type openStackSwift', ) .option( - '--gcsBucketRootPath', + '--gcsBucketRootPath <GCS BUCKET ROOT PATH>', 'Optional sub-directory to store files in Google cloud storage', ) .option( @@ -224,6 +233,11 @@ export function registerCommands(program: Command) { '--no-docker', 'Do not use Docker, run `mkdocs serve` in current user environment.', ) + .option( + '--site-name', + 'Name for site when using default MkDocs config', + 'Documentation Site', + ) .option('-p, --port <PORT>', 'Port to serve documentation locally', '8000') .option('-v --verbose', 'Enable verbose output.', false) .action(lazy(() => import('./serve/mkdocs').then(m => m.default))); @@ -250,6 +264,11 @@ export function registerCommands(program: Command) { '--no-docker', 'Do not use Docker, use MkDocs executable in current user environment.', ) + .option( + '--site-name', + 'Name for site when using default MkDocs config', + 'Documentation Site', + ) .option('--mkdocs-port <PORT>', 'Port for MkDocs server to use', '8000') .option('-v --verbose', 'Enable verbose output.', false) .option( diff --git a/packages/techdocs-cli/src/commands/serve/mkdocs.ts b/packages/techdocs-cli/src/commands/serve/mkdocs.ts index 9cc0aef3be..c061d4474f 100644 --- a/packages/techdocs-cli/src/commands/serve/mkdocs.ts +++ b/packages/techdocs-cli/src/commands/serve/mkdocs.ts @@ -19,6 +19,8 @@ import openBrowser from 'react-dev-utils/openBrowser'; import { createLogger } from '../../lib/utility'; import { runMkdocsServer } from '../../lib/mkdocsServer'; import { LogFunc, waitForSignal } from '../../lib/run'; +import { getMkdocsYml } from '@backstage/plugin-techdocs-node'; +import fs from 'fs-extra'; export default async function serveMkdocs(opts: OptionValues) { const logger = createLogger({ verbose: opts.verbose }); @@ -26,6 +28,12 @@ export default async function serveMkdocs(opts: OptionValues) { const dockerAddr = `http://0.0.0.0:${opts.port}`; const localAddr = `http://127.0.0.1:${opts.port}`; const expectedDevAddr = opts.docker ? dockerAddr : localAddr; + + const { path: mkdocsYmlPath, configIsTemporary } = await getMkdocsYml( + './', + opts.siteName, + ); + // We want to open browser only once based on a log. let boolOpenBrowserTriggered = false; @@ -70,4 +78,10 @@ export default async function serveMkdocs(opts: OptionValues) { // Keep waiting for user to cancel the process await waitForSignal([childProcess]); + + if (configIsTemporary) { + process.on('exit', async () => { + fs.rmSync(mkdocsYmlPath, {}); + }); + } } diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index d46c70678a..9e2e521546 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -22,6 +22,8 @@ import HTTPServer from '../../lib/httpServer'; import { runMkdocsServer } from '../../lib/mkdocsServer'; import { LogFunc, waitForSignal } from '../../lib/run'; import { createLogger } from '../../lib/utility'; +import { getMkdocsYml } from '@backstage/plugin-techdocs-node'; +import fs from 'fs-extra'; function findPreviewBundlePath(): string { try { @@ -64,6 +66,11 @@ export default async function serve(opts: OptionValues) { ? mkdocsDockerAddr : mkdocsLocalAddr; + const { path: mkdocsYmlPath, configIsTemporary } = await getMkdocsYml( + './', + opts.siteName, + ); + let mkdocsServerHasStarted = false; const mkdocsLogFunc: LogFunc = data => { // Sometimes the lines contain an unnecessary extra new line @@ -120,7 +127,7 @@ export default async function serve(opts: OptionValues) { const httpServer = new HTTPServer( previewAppPath, port, - opts.mkdocsPort, + mkdocsExpectedDevAddr, opts.verbose, ); @@ -140,4 +147,10 @@ export default async function serve(opts: OptionValues) { }); await waitForSignal([mkdocsChildProcess]); + + if (configIsTemporary) { + process.on('exit', async () => { + fs.rmSync(mkdocsYmlPath, {}); + }); + } } diff --git a/packages/techdocs-cli/src/lib/PublisherConfig.ts b/packages/techdocs-cli/src/lib/PublisherConfig.ts index 0d1c8fc8a9..803e40308b 100644 --- a/packages/techdocs-cli/src/lib/PublisherConfig.ts +++ b/packages/techdocs-cli/src/lib/PublisherConfig.ts @@ -17,7 +17,7 @@ import { ConfigReader } from '@backstage/config'; import { OptionValues } from 'commander'; -type Publisher = keyof typeof PublisherConfig['configFactories']; +type Publisher = keyof (typeof PublisherConfig)['configFactories']; type PublisherConfiguration = { [p in Publisher]?: any; } & { @@ -73,7 +73,7 @@ export class PublisherConfig { */ private static isKnownPublisher( type: string, - ): type is keyof typeof PublisherConfig['configFactories'] { + ): type is keyof (typeof PublisherConfig)['configFactories'] { return PublisherConfig.configFactories.hasOwnProperty(type); } @@ -94,6 +94,7 @@ export class PublisherConfig { ...(opts.awsEndpoint && { endpoint: opts.awsEndpoint }), ...(opts.awsS3ForcePathStyle && { s3ForcePathStyle: true }), ...(opts.awsS3sse && { sse: opts.awsS3sse }), + ...(opts.awsProxy && { httpsProxy: opts.awsProxy }), }, }; } diff --git a/packages/techdocs-cli/src/lib/httpServer.ts b/packages/techdocs-cli/src/lib/httpServer.ts index c4bd5b823a..08987f3f1d 100644 --- a/packages/techdocs-cli/src/lib/httpServer.ts +++ b/packages/techdocs-cli/src/lib/httpServer.ts @@ -23,26 +23,26 @@ export default class HTTPServer { private readonly proxyEndpoint: string; private readonly backstageBundleDir: string; private readonly backstagePort: number; - private readonly mkdocsPort: number; + private readonly mkdocsTargetAddress: string; private readonly verbose: boolean; constructor( backstageBundleDir: string, backstagePort: number, - mkdocsPort: number, + mkdocsTargetAddress: string, verbose: boolean, ) { this.proxyEndpoint = '/api/techdocs/'; this.backstageBundleDir = backstageBundleDir; this.backstagePort = backstagePort; - this.mkdocsPort = mkdocsPort; + this.mkdocsTargetAddress = mkdocsTargetAddress; this.verbose = verbose; } // Create a Proxy for mkdocs server private createProxy() { const proxy = httpProxy.createProxyServer({ - target: `http://localhost:${this.mkdocsPort}`, + target: this.mkdocsTargetAddress, }); return (request: http.IncomingMessage): [httpProxy, string] => { diff --git a/packages/techdocs-cli/src/lib/utility.ts b/packages/techdocs-cli/src/lib/utility.ts index 3f91f3c82d..00927fd1df 100644 --- a/packages/techdocs-cli/src/lib/utility.ts +++ b/packages/techdocs-cli/src/lib/utility.ts @@ -18,6 +18,8 @@ import { ParsedLocationAnnotation, } from '@backstage/plugin-techdocs-node'; import * as winston from 'winston'; +import { Writable } from 'stream'; +import { stdout } from 'process'; export const convertTechDocsRefToLocationAnnotation = ( techdocsRef: string, @@ -52,3 +54,17 @@ export const createLogger = ({ return logger; }; + +export const getLogStream = (logger: winston.Logger): Writable => { + if (process.env.LOG_LEVEL === 'debug') { + return stdout; + } + + return new Writable({ + defaultEncoding: 'utf8', + write(chunk, _encoding, next) { + logger.verbose(chunk.toString().trim()); + next(); + }, + }); +}; diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 5738586f5c..2a2ff57694 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/test-utils +## 1.2.6-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.11-next.0 + +## 1.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.10 + +## 1.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + ## 1.2.4 ### Patch Changes diff --git a/packages/test-utils/README.md b/packages/test-utils/README.md index d32ebcaf02..74e6d823d3 100644 --- a/packages/test-utils/README.md +++ b/packages/test-utils/README.md @@ -14,4 +14,4 @@ yarn add -D @backstage/test-utils ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/test-utils/alpha-api-report.md b/packages/test-utils/alpha-api-report.md new file mode 100644 index 0000000000..3e0c4aa6c2 --- /dev/null +++ b/packages/test-utils/alpha-api-report.md @@ -0,0 +1,14 @@ +## API Report File for "@backstage/test-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { PropsWithChildren } from 'react'; + +// @alpha +export const MockPluginProvider: ({ + children, +}: PropsWithChildren<{}>) => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index e11072f5a6..37650ff18a 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -26,7 +26,6 @@ import { JsonValue } from '@backstage/types'; import { MatcherFunction } from '@testing-library/react'; import { Observable } from '@backstage/types'; import { PermissionApi } from '@backstage/plugin-permission-react'; -import { PropsWithChildren } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RenderOptions } from '@testing-library/react'; @@ -167,11 +166,6 @@ export class MockPermissionApi implements PermissionApi { ): Promise<EvaluatePermissionResponse>; } -// @alpha -export const MockPluginProvider: ({ - children, -}: PropsWithChildren<{}>) => JSX.Element; - // @public export class MockStorageApi implements StorageApi { // (undocumented) diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index c3f9349390..ef57be6a57 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,12 +1,24 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "1.2.4", + "version": "1.2.6-next.0", "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "web-library" @@ -24,7 +36,7 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -57,10 +69,9 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/node": "^16.11.26", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ - "dist", - "alpha" + "dist" ] } diff --git a/packages/test-utils/src/alpha.ts b/packages/test-utils/src/alpha.ts new file mode 100644 index 0000000000..0b2a6a8d1c --- /dev/null +++ b/packages/test-utils/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './testUtils/MockPluginProvider'; diff --git a/packages/test-utils/src/testUtils/providers.tsx b/packages/test-utils/src/testUtils/MockPluginProvider.tsx similarity index 89% rename from packages/test-utils/src/testUtils/providers.tsx rename to packages/test-utils/src/testUtils/MockPluginProvider.tsx index 8bc4b5122d..25442ae267 100644 --- a/packages/test-utils/src/testUtils/providers.tsx +++ b/packages/test-utils/src/testUtils/MockPluginProvider.tsx @@ -15,7 +15,8 @@ */ import React, { PropsWithChildren } from 'react'; -import { createPlugin, PluginProvider } from '@backstage/core-plugin-api'; +import { PluginProvider } from '@backstage/core-plugin-api/alpha'; +import { createPlugin } from '@backstage/core-plugin-api'; /** * Mock for PluginProvider to use in unit tests diff --git a/packages/test-utils/src/testUtils/TestApiProvider.tsx b/packages/test-utils/src/testUtils/TestApiProvider.tsx index 51edf77cba..c212b94bea 100644 --- a/packages/test-utils/src/testUtils/TestApiProvider.tsx +++ b/packages/test-utils/src/testUtils/TestApiProvider.tsx @@ -91,6 +91,10 @@ export class TestApiRegistry implements ApiHolder { * It lets you provide any number of API implementations, without necessarily * having to fully implement each of the APIs. * + * @remarks + * todo: remove this remark tag and ship in the api-reference. There's some odd formatting going on when this is made into a markdown doc, that there's no line break between + * the emmited <p> for To the following </p> so what happens is that when parsing in docusaurus, it thinks that the code block is mdx rather than a code + * snippet. Just ommiting this from the report for now until we can work out how to fix laterr. * A migration from `ApiRegistry` and `ApiProvider` might look like this, from: * * ```tsx @@ -100,7 +104,7 @@ export class TestApiRegistry implements ApiHolder { * [identityApiRef, mockIdentityApi as unknown as IdentityApi] * ])} * > - * {...} + * ... * </ApiProvider> * ) * ``` @@ -110,7 +114,7 @@ export class TestApiRegistry implements ApiHolder { * ```tsx * renderInTestApp( * <TestApiProvider apis={[[identityApiRef, mockIdentityApi]]}> - * {...} + * ... * </TestApiProvider> * ) * ``` @@ -119,7 +123,7 @@ export class TestApiRegistry implements ApiHolder { * implements a subset of the `IdentityApi`. * * @public - **/ + */ export const TestApiProvider = <T extends any[]>( props: TestApiProviderProps<T>, ) => { diff --git a/packages/test-utils/src/testUtils/index.tsx b/packages/test-utils/src/testUtils/index.tsx index 8fc1395eb8..1510af106e 100644 --- a/packages/test-utils/src/testUtils/index.tsx +++ b/packages/test-utils/src/testUtils/index.tsx @@ -25,7 +25,6 @@ export { export type { TestAppOptions } from './appWrappers'; export * from './msw'; export * from './logCollector'; -export * from './providers'; export * from './testingLibrary'; export { TestApiProvider, TestApiRegistry } from './TestApiProvider'; export type { TestApiProviderProps } from './TestApiProvider'; diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index dcdcf2a6ae..a0ad669076 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/theme +## 0.2.17 + +### Patch Changes + +- b7705e176c: Use same table header color as @backstage/core-components Table to meet accessibility color contrast requirements. This change affects material-ui tables. + ## 0.2.16 ### Patch Changes diff --git a/packages/theme/README.md b/packages/theme/README.md index 0dc4059745..f50ed1cea6 100644 --- a/packages/theme/README.md +++ b/packages/theme/README.md @@ -14,4 +14,4 @@ yarn add @backstage/theme ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/theme/package.json b/packages/theme/package.json index f311aa7c1f..70f7cb2277 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.2.16", + "version": "0.2.17", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 5e222c0c3e..53f3a3ef2b 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -159,7 +159,7 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { head: { wordBreak: 'break-word', overflow: 'hidden', - color: 'rgb(179, 179, 179)', + color: theme.palette.textSubtle, fontWeight: 'normal', lineHeight: '1', }, diff --git a/plugins/.changeset/dull-adults-drum.md b/plugins/.changeset/dull-adults-drum.md new file mode 100644 index 0000000000..088eb1539e --- /dev/null +++ b/plugins/.changeset/dull-adults-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-adr': patch +--- + +Updated readme instructions diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md index 173baefd48..9bd8a9576a 100644 --- a/plugins/adr-backend/CHANGELOG.md +++ b/plugins/adr-backend/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/plugin-adr-backend +## 0.3.1-next.0 + +### Patch Changes + +- 8bf24946c6: Adjust express dependencies to be the same as the rest of the project +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.7-next.0 + - @backstage/plugin-search-common@1.2.1 + +## 0.3.0 + +### Minor Changes + +- 0a32911d8a: Display title, status and date in ADR navigation, sourced from ADR content and reverse order. + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.6 + - @backstage/plugin-search-common@1.2.1 + +## 0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.6-next.1 + - @backstage/plugin-search-common@1.2.1 + +## 0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.6-next.0 + - @backstage/plugin-search-common@1.2.1 + +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-adr-common@0.2.6-next.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/adr-backend/README.md b/plugins/adr-backend/README.md index b751a3b56d..29fead7c84 100644 --- a/plugins/adr-backend/README.md +++ b/plugins/adr-backend/README.md @@ -19,9 +19,8 @@ Here's how to get the backend up and running: 1. First we need to add the `@backstage/plugin-adr-backend` package to your backend: ```sh -# From the Backstage root directory -cd packages/backend -yarn add @backstage/plugin-adr-backend +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-adr-backend ``` 2. Then we will create a new file named `packages/backend/src/plugins/adr.ts`, and add the diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index 448afabdfe..719b3808a7 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr-backend", - "version": "0.2.5", + "version": "0.3.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,9 +36,9 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-adr-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", - "@types/express": "^4.17.15", - "express": "^4.18.2", - "express-promise-router": "^4.1.1", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", "luxon": "^3.0.0", "marked": "^4.0.14", "node-fetch": "^2.6.5", @@ -49,7 +49,7 @@ "@backstage/cli": "workspace:^", "@types/marked": "^4.0.0", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/adr-backend/src/search/createMadrParser.ts b/plugins/adr-backend/src/search/createMadrParser.ts index 6b77ec2fa7..1bc3c852e9 100644 --- a/plugins/adr-backend/src/search/createMadrParser.ts +++ b/plugins/adr-backend/src/search/createMadrParser.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import { DateTime } from 'luxon'; -import { marked } from 'marked'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { MADR_DATE_FORMAT } from '@backstage/plugin-adr-common'; import { AdrParser } from './types'; +import { madrParser } from './madrParser'; const applyArgsToFormat = ( format: string, @@ -65,43 +64,12 @@ export const createMadrParser = ( const dateFormat = options.dateFormat ?? MADR_DATE_FORMAT; return async ({ entity, content, path }) => { - const tokens = marked.lexer(content); - if (!tokens.length) { - throw new Error('ADR has no content'); - } - - // First h1 header should contain ADR title - const adrTitle = ( - tokens.find( - t => t.type === 'heading' && t.depth === 1, - ) as marked.Tokens.Heading - )?.text; - - // First list should contain status & date metadata (if defined) - const listTokens = ( - tokens.find(t => t.type === 'list') as marked.Tokens.List - )?.items; - const adrStatus = listTokens - .find(t => /^status:/i.test(t.text)) - ?.text.replace(/^status:/i, '') - .trim() - .toLocaleLowerCase('en-US'); - const adrDateTime = DateTime.fromFormat( - listTokens - .find(t => /^date:/i.test(t.text)) - ?.text.replace(/^date:/i, '') - .trim() ?? '', - dateFormat, - ); - const adrDate = adrDateTime.isValid - ? adrDateTime.toFormat(MADR_DATE_FORMAT) - : undefined; - + const madr = madrParser(content, dateFormat); return { - title: adrTitle ?? path.replace(/\.md$/, ''), + title: madr.title ?? path.replace(/\.md$/, ''), text: content, - status: adrStatus, - date: adrDate, + status: madr.status, + date: madr.date, entityRef: stringifyEntityRef(entity), entityTitle: entity.metadata.title, location: applyArgsToFormat(locationTemplate, { diff --git a/plugins/adr-backend/src/search/madrParser.ts b/plugins/adr-backend/src/search/madrParser.ts new file mode 100644 index 0000000000..30672028fe --- /dev/null +++ b/plugins/adr-backend/src/search/madrParser.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DateTime } from 'luxon'; +import { marked } from 'marked'; +import { MADR_DATE_FORMAT } from '@backstage/plugin-adr-common'; + +export const madrParser = (content: string, dateFormat = MADR_DATE_FORMAT) => { + const tokens = marked.lexer(content); + if (!tokens.length) { + throw new Error('ADR has no content'); + } + + // First h1 header should contain ADR title + const adrTitle = ( + tokens.find( + t => t.type === 'heading' && t.depth === 1, + ) as marked.Tokens.Heading + )?.text; + + // First list should contain status & date metadata (if defined) + const listTokens = (tokens.find(t => t.type === 'list') as marked.Tokens.List) + ?.items; + const adrStatus = listTokens + ?.find(t => /^status:/i.test(t.text)) + ?.text.replace(/^status:/i, '') + .trim() + .toLocaleLowerCase('en-US'); + + const adrDateTime = DateTime.fromFormat( + listTokens + ?.find(t => /^date:/i.test(t.text)) + ?.text.replace(/^date:/i, '') + .trim() ?? '', + dateFormat, + ); + const adrDate = adrDateTime?.isValid + ? adrDateTime.toFormat(MADR_DATE_FORMAT) + : undefined; + + return { + title: adrTitle, + status: adrStatus, + date: adrDate, + }; +}; diff --git a/plugins/adr-backend/src/service/router.test.ts b/plugins/adr-backend/src/service/router.test.ts index fbbd6e8532..6836343882 100644 --- a/plugins/adr-backend/src/service/router.test.ts +++ b/plugins/adr-backend/src/service/router.test.ts @@ -107,6 +107,8 @@ class MockCacheClient implements CacheClient { async delete(key: string) { delete this.itemRegistry[key]; } + + withOptions = () => this; } describe('createRouter', () => { @@ -156,8 +158,8 @@ describe('createRouter', () => { data: [ { type: 'file', - name: 'testFile001.txt', - path: 'folder/testFile001.txt', + name: 'testFile002.txt', + path: 'testFile002.txt', }, { type: 'file', @@ -166,8 +168,8 @@ describe('createRouter', () => { }, { type: 'file', - name: 'testFile002.txt', - path: 'testFile002.txt', + name: 'testFile001.txt', + path: 'folder/testFile001.txt', }, ], }; diff --git a/plugins/adr-backend/src/service/router.ts b/plugins/adr-backend/src/service/router.ts index e6603ab346..ed02a76e13 100644 --- a/plugins/adr-backend/src/service/router.ts +++ b/plugins/adr-backend/src/service/router.ts @@ -19,6 +19,7 @@ import { NotModifiedError, stringifyError } from '@backstage/errors'; import { Logger } from 'winston'; import express from 'express'; import Router from 'express-promise-router'; +import { madrParser } from '../search/madrParser'; /** @public */ export type AdrRouterOptions = { @@ -59,13 +60,20 @@ export async function createRouter( etag: cachedTree?.etag, }); const files = await treeGetResponse.files(); - const data = files.map(file => { - return { - type: 'file', - name: file.path.substring(file.path.lastIndexOf('/') + 1), - path: file.path, - }; - }); + const data = await Promise.all( + files + .map(async file => { + const fileContent = await file.content(); + const adrInfo = madrParser(fileContent.toString()); + return { + type: 'file', + name: file.path.substring(file.path.lastIndexOf('/') + 1), + path: file.path, + ...adrInfo, + }; + }) + .reverse(), + ); await cacheClient.set(urlToProcess, { data, diff --git a/plugins/adr-common/CHANGELOG.md b/plugins/adr-common/CHANGELOG.md index 594c6d39c2..b04b7a0c64 100644 --- a/plugins/adr-common/CHANGELOG.md +++ b/plugins/adr-common/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-adr-common +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/integration@1.4.2 + - @backstage/plugin-search-common@1.2.1 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/integration@1.4.2 + - @backstage/plugin-search-common@1.2.1 + +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/integration@1.4.2 + - @backstage/plugin-search-common@1.2.1 + +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json index 41060fb5ca..b2557c5407 100644 --- a/plugins/adr-common/package.json +++ b/plugins/adr-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-adr-common", "description": "Common functionalities for the adr plugin", - "version": "0.2.5", + "version": "0.2.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md index a150f7d3eb..c22c566b6c 100644 --- a/plugins/adr/CHANGELOG.md +++ b/plugins/adr/CHANGELOG.md @@ -1,5 +1,88 @@ # @backstage/plugin-adr +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-adr-common@0.2.7-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## 0.4.0 + +### Minor Changes + +- 0a32911d8a: Display title, status and date in ADR navigation, sourced from ADR content and reverse order. +- ec34b535c0: The `AdrSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- ec34b535c0: Support displaying an icon on `AdrSearchResultListItem` +- 496cf6aff0: Render the common `<MissingAnnotationEmptyState />` component when the `backstage.io/adr-location` annotation is missing from the component +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-adr-common@0.2.6 + - @backstage/plugin-search-common@1.2.1 + +## 0.4.0-next.2 + +### Minor Changes + +- ec34b535c0: The `AdrSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- ec34b535c0: Support displaying an icon on `AdrSearchResultListItem` +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.6-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + +## 0.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.6-next.0 + - @backstage/plugin-search-common@1.2.1 + +## 0.3.1-next.0 + +### Patch Changes + +- 496cf6aff0: Render the common `<MissingAnnotationEmptyState />` component when the `backstage.io/adr-location` annotation is missing from the component +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-adr-common@0.2.6-next.0 + - @backstage/integration-react@1.1.9 + ## 0.3.0 ### Minor Changes diff --git a/plugins/adr/README.md b/plugins/adr/README.md index b9cc5d84ba..7feb9b03b0 100644 --- a/plugins/adr/README.md +++ b/plugins/adr/README.md @@ -4,6 +4,8 @@ Welcome to the ADR plugin! This plugin allows you to browse ADRs associated with your entities as well as a way to discover ADRs across others entities via Backstage Search. Use this to learn from the past experience of other projects to guide your own architecture decisions. +![ADR tab](./docs/adr-tab.png) + ## Setup 1. Install this plugin: @@ -26,7 +28,7 @@ yarn --cwd packages/app add @backstage/plugin-adr import { EntityAdrContent, isAdrAvailable } from '@backstage/plugin-adr'; ... - +// Note: Add to any other Pages as well (e.g. defaultEntityPage and websiteEntityPage) const serviceEntityPage = ( <EntityLayout> {/* other tabs... */} @@ -67,16 +69,46 @@ Afterwards, add the following code snippet to use `AdrSearchResultListItem` when ```tsx // In packages/app/src/components/search/SearchPage.tsx import { AdrSearchResultListItem } from '@backstage/plugin-adr'; +import { AdrDocument } from '@backstage/plugin-adr-common'; +... +// Optional - Add type to side pane +<SearchType.Accordion + name="Result Type" + defaultValue="software-catalog" + types={[ + ... + { + value: 'adr', + name: 'Architecture Decision Records', + icon: <DocsIcon />, + }, + ]} +/> ... -case 'adr': - return ( - <AdrSearchResultListItem - key={document.location} - result={document} - /> - ); +// In results +<SearchResult> + {({ results }) => ( + <List> + {results.map(({ type, document, highlight, rank }) => { + switch (type) { + ... + case 'adr': + return ( + <AdrSearchResultListItem + key={document.location} + // Not required if you're leveraging the new search results extensions available in v1.11+ + // https://backstage.io/docs/features/search/how-to-guides#2-using-an-extension-in-your-backstage-app + result={document as AdrDocument} + /> + ); + ... + } + })} + </List> + )} +</SearchResult> ``` ## Custom ADR formats diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md index d6480cd090..1d07edc4ce 100644 --- a/plugins/adr/api-report.md +++ b/plugins/adr/api-report.md @@ -11,6 +11,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { isAdrAvailable } from '@backstage/plugin-adr-common'; +import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -51,6 +52,9 @@ export type AdrFileInfo = { type: string; path: string; name: string; + title?: string; + status?: string; + date?: string; }; // @public @@ -82,12 +86,18 @@ export type AdrReadResult = { }; // @public -export function AdrSearchResultListItem(props: { +export const AdrSearchResultListItem: ( + props: AdrSearchResultListItemProps, +) => JSX.Element | null; + +// @public (undocumented) +export type AdrSearchResultListItemProps = { lineClamp?: number; highlight?: ResultHighlight; + icon?: ReactNode; rank?: number; - result: AdrDocument; -}): JSX.Element; + result?: AdrDocument; +}; // @public export const EntityAdrContent: (props: { diff --git a/plugins/adr/docs/adr-tab.png b/plugins/adr/docs/adr-tab.png new file mode 100644 index 0000000000..d9dd728221 Binary files /dev/null and b/plugins/adr/docs/adr-tab.png differ diff --git a/plugins/adr/package.json b/plugins/adr/package.json index 22c9018583..3910fbdef9 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr", - "version": "0.3.0", + "version": "0.4.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,6 +39,7 @@ "remark-gfm": "^3.0.1" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0", "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, @@ -53,7 +54,7 @@ "@types/git-url-parse": "^9.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/adr/src/api/types.ts b/plugins/adr/src/api/types.ts index e4632e171a..0af35cc6df 100644 --- a/plugins/adr/src/api/types.ts +++ b/plugins/adr/src/api/types.ts @@ -30,6 +30,15 @@ export type AdrFileInfo = { /** The name of the ADR file. */ name: string; + + /** The title of the ADR. */ + title?: string; + + /** The status of the ADR. */ + status?: string; + + /** The date of the ADR. */ + date?: string; }; /** diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx index d1db286943..eb5b4aed21 100644 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -35,6 +35,7 @@ import { } from '@backstage/plugin-adr-common'; import { useEntity } from '@backstage/plugin-catalog-react'; import { + Chip, Grid, List, ListItem, @@ -67,7 +68,9 @@ export const EntityAdrContent = (props: { const classes = useStyles(); const { entity } = useEntity(); const rootLink = useRouteRef(rootRouteRef); - const [adrList, setAdrList] = useState<string[]>([]); + const [adrList, setAdrList] = useState< + { name: string; title?: string; status?: string; date?: string }[] + >([]); const [searchParams, setSearchParams] = useSearchParams(); const scmIntegrations = useApi(scmIntegrationsApiRef); const adrApi = useApi(adrApiRef); @@ -79,10 +82,10 @@ export const EntityAdrContent = (props: { }, [entity, scmIntegrations]); const selectedAdr = - adrList.find(adr => adr === searchParams.get('record')) ?? ''; + adrList.find(adr => adr.name === searchParams.get('record'))?.name ?? ''; useEffect(() => { if (adrList.length && !selectedAdr) { - searchParams.set('record', adrList[0]); + searchParams.set('record', adrList[0].name); setSearchParams(searchParams, { replace: true }); } }); @@ -100,11 +103,22 @@ export const EntityAdrContent = (props: { ? filePathFilterFn(item.name) : madrFilePathFilter(item.name)), ) - .map(({ name }: { name: string }) => name); + .map(({ name, title, status, date }) => ({ name, title, status, date })); setAdrList(adrs); }, [filePathFilterFn, value]); + const getChipColor = (status: string) => { + switch (status) { + case 'accepted': + return 'primary'; + case 'rejected' || 'deprecated': + return 'secondary'; + default: + return 'default'; + } + }; + return ( <Content> <ContentHeader title="Architecture Decision Records"> @@ -133,13 +147,24 @@ export const EntityAdrContent = (props: { key={idx} button component={Link} - to={`${rootLink()}?record=${adr}`} - selected={selectedAdr === adr} + to={`${rootLink()}?record=${adr.name}`} + selected={selectedAdr === adr.name} > <ListItemText - secondaryTypographyProps={{ noWrap: true }} - secondary={adr.replace(/\.md$/, '')} + primaryTypographyProps={{ + style: { whiteSpace: 'normal' }, + }} + primary={adr.title ?? adr?.name.replace(/\.md$/, '')} + secondary={adr.date} /> + {adr.status && ( + <Chip + label={adr.status} + size="small" + variant="outlined" + color={getChipColor(adr.status)} + /> + )} </ListItem> ))} </List> diff --git a/plugins/adr/src/index.ts b/plugins/adr/src/index.ts index 2ec76b0fd6..f24b3ac531 100644 --- a/plugins/adr/src/index.ts +++ b/plugins/adr/src/index.ts @@ -30,5 +30,5 @@ export type { } from './api'; export { isAdrAvailable } from '@backstage/plugin-adr-common'; export * from './components/AdrReader'; -export { adrPlugin, EntityAdrContent } from './plugin'; -export * from './search'; +export { adrPlugin, AdrSearchResultListItem, EntityAdrContent } from './plugin'; +export type { AdrSearchResultListItemProps } from './search'; diff --git a/plugins/adr/src/plugin.ts b/plugins/adr/src/plugin.ts index f23df826cd..05e4adec4a 100644 --- a/plugins/adr/src/plugin.ts +++ b/plugins/adr/src/plugin.ts @@ -21,7 +21,9 @@ import { createRoutableExtension, discoveryApiRef, } from '@backstage/core-plugin-api'; +import { createSearchResultListItemExtension } from '@backstage/plugin-search-react'; import { rootRouteRef } from './routes'; +import { AdrSearchResultListItemProps } from './search'; /** * The Backstage plugin that holds ADR specific components @@ -57,3 +59,21 @@ export const EntityAdrContent = adrPlugin.provide( mountPoint: rootRouteRef, }), ); + +/** + * React extension used to render results on Search page or modal + * + * @public + */ +export const AdrSearchResultListItem: ( + props: AdrSearchResultListItemProps, +) => JSX.Element | null = adrPlugin.provide( + createSearchResultListItemExtension({ + name: 'AdrSearchResultListItem', + component: () => + import('./search/AdrSearchResultListItem').then( + m => m.AdrSearchResultListItem, + ), + predicate: result => result.type === 'adr', + }), +); diff --git a/plugins/adr/src/search/AdrSearchResultListItem.tsx b/plugins/adr/src/search/AdrSearchResultListItem.tsx index 3a11b5cf9a..118333a439 100644 --- a/plugins/adr/src/search/AdrSearchResultListItem.tsx +++ b/plugins/adr/src/search/AdrSearchResultListItem.tsx @@ -14,19 +14,19 @@ * limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { Box, Chip, Divider, ListItem, + ListItemIcon, ListItemText, makeStyles, } from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; import { parseEntityRef } from '@backstage/catalog-model'; import { Link } from '@backstage/core-components'; -import { useAnalytics } from '@backstage/core-plugin-api'; import { AdrDocument } from '@backstage/plugin-adr-common'; import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; import { ResultHighlight } from '@backstage/plugin-search-common'; @@ -43,35 +43,36 @@ const useStyles = makeStyles({ }, }); +/** + * @public + */ +export type AdrSearchResultListItemProps = { + lineClamp?: number; + highlight?: ResultHighlight; + icon?: ReactNode; + rank?: number; + result?: AdrDocument; +}; + /** * A component to display an ADR search result. * @public */ -export function AdrSearchResultListItem(props: { - lineClamp?: number; - highlight?: ResultHighlight; - rank?: number; - result: AdrDocument; -}) { - const { lineClamp = 5, highlight, rank, result } = props; +export function AdrSearchResultListItem(props: AdrSearchResultListItemProps) { + const { lineClamp = 5, highlight, icon, result } = props; const classes = useStyles(); - const analytics = useAnalytics(); - const handleClick = () => { - analytics.captureEvent('discover', result.title, { - attributes: { to: result.location }, - value: rank, - }); - }; + if (!result) return null; return ( <> <ListItem alignItems="flex-start" className={classes.flexContainer}> + {icon && <ListItemIcon>{icon}</ListItemIcon>} <ListItemText className={classes.itemText} primaryTypographyProps={{ variant: 'h6' }} primary={ - <Link noTrack to={result.location} onClick={handleClick}> + <Link noTrack to={result.location}> {highlight?.fields.title ? ( <HighlightedSearchResultText text={highlight?.fields.title || ''} diff --git a/plugins/adr/src/search/index.ts b/plugins/adr/src/search/index.ts index 519d8cb91f..a20f553959 100644 --- a/plugins/adr/src/search/index.ts +++ b/plugins/adr/src/search/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './AdrSearchResultListItem'; +export type { AdrSearchResultListItemProps } from './AdrSearchResultListItem'; diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md index b8d0ddf0ac..821f2df769 100644 --- a/plugins/airbrake-backend/CHANGELOG.md +++ b/plugins/airbrake-backend/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-airbrake-backend +## 0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + +## 0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + +## 0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## 0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + +## 0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.2.13 ### Patch Changes diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json index 8003d57306..6f79200b0e 100644 --- a/plugins/airbrake-backend/package.json +++ b/plugins/airbrake-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake-backend", - "version": "0.2.13", + "version": "0.2.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "@backstage/cli": "workspace:^", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index d063c1146b..493a4ecd34 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-airbrake +## 0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/test-utils@1.2.6-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/dev-utils@1.0.13-next.0 + - @backstage/theme@0.2.17 + +## 0.3.15 + +### Patch Changes + +- 41377156d0: Adds a boolean helper function to airbrake plugin for use on the EntityPage to show/hide airbrake tab depending on whether the entity's `catalog-info.yaml` has an airbrake id set in the metadata +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/dev-utils@1.0.12 + - @backstage/test-utils@1.2.5 + +## 0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/dev-utils@1.0.12-next.2 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.3.15-next.1 + +### Patch Changes + +- 41377156d0: Adds a boolean helper function to airbrake plugin for use on the EntityPage to show/hide airbrake tab depending on whether the entity's catalog-info.yml has an airbrake id set in the metadata +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/dev-utils@1.0.12-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + +## 0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/dev-utils@1.0.12-next.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/airbrake/README.md b/plugins/airbrake/README.md index bc30ca7896..f37ab08b75 100644 --- a/plugins/airbrake/README.md +++ b/plugins/airbrake/README.md @@ -18,14 +18,21 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https: yarn add --cwd packages/backend @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: +3. Add the `EntityAirbrakeContent` and `isAirbrakeAvailable` 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'; + import { + EntityAirbrakeContent, + isAirbrakeAvailable, + } from '@backstage/plugin-airbrake'; const serviceEntityPage = ( <EntityLayoutWrapper> - <EntityLayout.Route path="/airbrake" title="Airbrake"> + <EntityLayout.Route + if={isAirbrakeAvailable} + path="/airbrake" + title="Airbrake" + > <EntityAirbrakeContent /> </EntityLayout.Route> </EntityLayoutWrapper> @@ -33,7 +40,11 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https: const websiteEntityPage = ( <EntityLayoutWrapper> - <EntityLayout.Route path="/airbrake" title="Airbrake"> + <EntityLayout.Route + if={isAirbrakeAvailable} + path="/airbrake" + title="Airbrake" + > <EntityAirbrakeContent /> </EntityLayout.Route> </EntityLayoutWrapper> @@ -41,7 +52,11 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https: const defaultEntityPage = ( <EntityLayoutWrapper> - <EntityLayout.Route path="/airbrake" title="Airbrake"> + <EntityLayout.Route + if={isAirbrakeAvailable} + path="/airbrake" + title="Airbrake" + > <EntityAirbrakeContent /> </EntityLayout.Route> </EntityLayoutWrapper> diff --git a/plugins/airbrake/api-report.md b/plugins/airbrake/api-report.md index e48fbf9cde..773e2a029a 100644 --- a/plugins/airbrake/api-report.md +++ b/plugins/airbrake/api-report.md @@ -6,6 +6,7 @@ /// <reference types="react" /> import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -19,4 +20,7 @@ export const airbrakePlugin: BackstagePlugin< // @public export const EntityAirbrakeContent: () => JSX.Element; + +// @public +export const isAirbrakeAvailable: (entity: Entity) => boolean; ``` diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 8b793ef129..22d961419c 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.3.14", + "version": "0.3.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -48,7 +48,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/airbrake/src/components/isAirbrakeAvailable.ts b/plugins/airbrake/src/components/isAirbrakeAvailable.ts new file mode 100644 index 0000000000..01d59668ce --- /dev/null +++ b/plugins/airbrake/src/components/isAirbrakeAvailable.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { AIRBRAKE_PROJECT_ID_ANNOTATION } from './useProjectId'; + +/** + * Utility function to determine if the given entity has an Airbrake ID set in the repos catalog-info.yml . + * @public + */ +export const isAirbrakeAvailable = (entity: Entity) => + Boolean(entity.metadata.annotations?.[AIRBRAKE_PROJECT_ID_ANNOTATION]); diff --git a/plugins/airbrake/src/index.ts b/plugins/airbrake/src/index.ts index 2b2406776b..717d544dc9 100644 --- a/plugins/airbrake/src/index.ts +++ b/plugins/airbrake/src/index.ts @@ -22,3 +22,4 @@ export { airbrakePlugin } from './plugin'; export { EntityAirbrakeContent } from './extensions'; +export { isAirbrakeAvailable } from './components/isAirbrakeAvailable'; diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index faafee7664..d3629f0406 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-allure +## 0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.1.31 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.1.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.30 ### Patch Changes diff --git a/plugins/allure/README.md b/plugins/allure/README.md index b9a8435a34..3a1137fc39 100644 --- a/plugins/allure/README.md +++ b/plugins/allure/README.md @@ -4,16 +4,9 @@ Welcome to the Backstage Allure plugin. This plugin add an entity service page t ## Install -Run the below command from the `app` package directory. - ```shell -yarn add @backstage/plugin-allure -``` - -alternatively, you can execute below command from the root directory of your Backstage app. - -```shell -yarn workspace app add @backstage/plugin-allure +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-allure ``` ## Configure diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 4700a078c9..98296190b7 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.30", + "version": "0.1.32-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -47,7 +47,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index d5e21c3b19..02bed18551 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-analytics-module-ga +## 0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.1.26 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + +## 0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.1.25 ### Patch Changes diff --git a/plugins/analytics-module-ga/README.md b/plugins/analytics-module-ga/README.md index c2d79aa45d..d6e2642e2b 100644 --- a/plugins/analytics-module-ga/README.md +++ b/plugins/analytics-module-ga/README.md @@ -9,7 +9,12 @@ This plugin contains no other functionality. ## Installation 1. Install the plugin package in your Backstage app: - `cd packages/app && yarn add @backstage/plugin-analytics-module-ga` + +```sh +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-analytics-module-ga +``` + 2. Wire up the API implementation to your App: ```tsx diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 01d9e78cfa..2cf706b25e 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.25", + "version": "0.1.27-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,7 +45,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index 0b4287a4fb..50ceff31ac 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-apache-airflow +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + +## 0.2.8 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 4bdd6bcb3c: Prefer Link over plain anchors +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/core-plugin-api@1.4.0 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + +## 0.2.8-next.0 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + ## 0.2.7 ### Patch Changes diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index b22e44e7e6..b249948e90 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.2.7", + "version": "0.2.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx index 2660e9380e..2cd136c7fa 100644 --- a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -15,6 +15,7 @@ */ import { + Link, Progress, StatusError, StatusOK, @@ -144,13 +145,14 @@ export const DenseTable = ({ dags, rowClick }: DenseTableProps) => { { title: 'Link', field: 'dagUrl', - render: (row: Partial<DagTableRow>) => ( - <a href={row.dagUrl}> - <IconButton aria-label="details"> - <OpenInBrowserIcon /> - </IconButton> - </a> - ), + render: (row: Partial<DagTableRow>) => + !row.dagUrl ? null : ( + <Link to={row.dagUrl}> + <IconButton aria-label="details"> + <OpenInBrowserIcon /> + </IconButton> + </Link> + ), width: '5%', disableClick: true, hidden: hiddenColumns.some(field => field === 'dagUrl'), diff --git a/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx index 14e8148b50..7bd8445766 100644 --- a/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx +++ b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx @@ -26,7 +26,7 @@ const timeDeltaToLabel = (delta: TimeDelta): string => { let label = ''; const date = new Date(0); date.setSeconds(delta.seconds); - const time = date.toISOString().substr(11, 8); + const time = date.toISOString().slice(11, 11 + 8); if (delta.days === 0) { label = `${time}`; } else if (delta.days === 1) { diff --git a/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md b/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md index 4e030b2264..f1c0b0499b 100644 --- a/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md +++ b/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-api-docs-module-protoc-gen-doc +## 0.1.1 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend + ## 0.1.0 ### Minor Changes diff --git a/plugins/api-docs-module-protoc-gen-doc/README.md b/plugins/api-docs-module-protoc-gen-doc/README.md index 4d90cd74c2..a921fafc0b 100644 --- a/plugins/api-docs-module-protoc-gen-doc/README.md +++ b/plugins/api-docs-module-protoc-gen-doc/README.md @@ -6,8 +6,9 @@ This package contains ApiDefinitionWidgets for the following projects: ## Setup -``` -yarn add @backstage/plugin-api-docs-module-protoc-gen-doc +```sh +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-api-docs-module-protoc-gen-doc ``` ## Add the GrpcDocsApiWidget to your apis diff --git a/plugins/api-docs-module-protoc-gen-doc/package.json b/plugins/api-docs-module-protoc-gen-doc/package.json index 072304d2cf..0fa001ff0e 100644 --- a/plugins/api-docs-module-protoc-gen-doc/package.json +++ b/plugins/api-docs-module-protoc-gen-doc/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs-module-protoc-gen-doc", "description": "Additional functionalities for the api-docs plugin that renders the output of the protoc-gen-doc", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index dcea892b5b..8f6878bb97 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,75 @@ # @backstage/plugin-api-docs +## 0.9.1-next.0 + +### Patch Changes + +- 095e755d9b: Add ApiDefinitionDialog component for fast access to the API definition in ProvidedApiCards and ConsumedApiCards. +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.9.0 + +### Minor Changes + +- 9ed53218a5: Adds a new tRPC API definition widget which users can utilize to integrate their tRPC API definitions into Backstage. + +### Patch Changes + +- 60996f8aba: Updating the readme with better `csp` instructions +- 6856ab98f4: Updated dependency `@asyncapi/react-component` to `1.0.0-next.47`. +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.9.0-next.2 + +### Minor Changes + +- 9ed53218a5: Adds a new tRPC API definition widget which users can utilize to integrate their tRPC API definitions into Backstage. + +### Patch Changes + +- 6856ab98f4: Updated dependency `@asyncapi/react-component` to `1.0.0-next.47`. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.8.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.8.15-next.0 + +### Patch Changes + +- 60996f8aba: Updating the readme with better `csp` instructions +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + ## 0.8.14 ### Patch Changes diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md index bea820cb61..27e19b218d 100644 --- a/plugins/api-docs/README.md +++ b/plugins/api-docs/README.md @@ -164,6 +164,7 @@ This also may require you to adjust `Content Security Policy` header settings of ```yaml script-src: - "'self'" + - "'unsafe-eval'" # this is required for scaffolder usage, and ajv validation. - "'sha256-GeDavzSZ8O71Jggf/pQkKbt52dfZkrdNMQ3e+Ox+AkI='" # oauth2-redirect.html ``` diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index 0872d97355..c00f9f8e00 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -20,6 +20,13 @@ import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // @public (undocumented) export const ApiDefinitionCard: () => JSX.Element; +// @public +export function ApiDefinitionDialog(props: { + open: boolean; + entity: ApiEntity; + onClose: () => void; +}): JSX.Element; + // @public (undocumented) export type ApiDefinitionWidget = { type: string; @@ -173,4 +180,14 @@ export const ProvidedApisCard: (props: { export const ProvidingComponentsCard: (props: { variant?: InfoCardVariants; }) => JSX.Element; + +// @public (undocumented) +export const TrpcApiDefinitionWidget: ( + props: TrpcApiDefinitionWidgetProps, +) => JSX.Element; + +// @public (undocumented) +export type TrpcApiDefinitionWidgetProps = { + definition: string; +}; ``` diff --git a/plugins/api-docs/dev/index.tsx b/plugins/api-docs/dev/index.tsx index 9975e91e98..995aa9c2fb 100644 --- a/plugins/api-docs/dev/index.tsx +++ b/plugins/api-docs/dev/index.tsx @@ -32,6 +32,7 @@ import graphqlApiEntity from './graphql-example-api.yaml'; import invalidLanguageApiEntity from './invalid-language-example-api.yaml'; import openapiApiEntity from './openapi-example-api.yaml'; import otherApiEntity from './other-example-api.yaml'; +import trpcApiEntity from './trpc-example-api.yaml'; const mockEntities = [ openapiApiEntity, @@ -39,6 +40,7 @@ const mockEntities = [ graphqlApiEntity, invalidLanguageApiEntity, otherApiEntity, + trpcApiEntity, ] as unknown as Entity[]; createDevApp() @@ -137,4 +139,17 @@ createDevApp() </Page> ), }) + .addPage({ + title: 'tRPC', + element: ( + <Page themeId="home"> + <Header title="tRPC" /> + <Content> + <EntityProvider entity={trpcApiEntity as any as Entity}> + <EntityApiDefinitionCard /> + </EntityProvider> + </Content> + </Page> + ), + }) .render(); diff --git a/plugins/api-docs/dev/trpc-example-api.yaml b/plugins/api-docs/dev/trpc-example-api.yaml new file mode 100644 index 0000000000..f5807537b3 --- /dev/null +++ b/plugins/api-docs/dev/trpc-example-api.yaml @@ -0,0 +1,25 @@ +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: hello-world-trpc + description: Hello World example for tRPC +spec: + type: trpc + lifecycle: experimental + owner: team-c + definition: | + import { z } from 'zod'; + import { publicProcedure, router } from '../trpc'; + + export const apiRouter = router({ + version: publicProcedure.query(() => { + return { version: '0.42.0' }; + }), + hello: publicProcedure + .input(z.object({ username: z.string().nullish() }).nullish()) + .query(({ input, ctx }) => { + return { + text: `hello ${input?.username ?? ctx.user?.name ?? 'world'}`, + }; + }), + }); diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 334124c771..7660e62320 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.8.14", + "version": "0.9.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@asyncapi/react-component": "1.0.0-next.44", + "@asyncapi/react-component": "1.0.0-next.47", "@backstage/catalog-model": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", @@ -65,7 +65,7 @@ "@types/node": "^16.11.26", "@types/swagger-ui-react": "^4.1.1", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionWidget.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionWidget.tsx index 03a56eaa78..8d7d7a7729 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionWidget.tsx @@ -18,6 +18,7 @@ import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget'; import { GraphQlDefinitionWidget } from '../GraphQlDefinitionWidget'; import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget'; import { GrpcApiDefinitionWidget } from '../GrpcApiDefinitionWidget'; +import { TrpcApiDefinitionWidget } from '../TrpcDefinitionWidget'; /** @public */ export type ApiDefinitionWidget = { @@ -61,5 +62,12 @@ export function defaultDefinitionWidgets(): ApiDefinitionWidget[] { <GrpcApiDefinitionWidget definition={definition} /> ), }, + { + type: 'trpc', + title: 'tRPC', + component: definition => ( + <TrpcApiDefinitionWidget definition={definition} /> + ), + }, ]; } diff --git a/plugins/api-docs/src/components/ApiDefinitionDialog/ApiDefinitionDialog.tsx b/plugins/api-docs/src/components/ApiDefinitionDialog/ApiDefinitionDialog.tsx new file mode 100644 index 0000000000..2f82a0cc5a --- /dev/null +++ b/plugins/api-docs/src/components/ApiDefinitionDialog/ApiDefinitionDialog.tsx @@ -0,0 +1,176 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApiEntity } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core-plugin-api'; +import { BackstageTheme } from '@backstage/theme'; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + makeStyles, + Tab, + Tabs, + Typography, +} from '@material-ui/core'; +import React, { useEffect } from 'react'; +import { apiDocsConfigRef } from '../../config'; +import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget'; + +const useStyles = makeStyles<BackstageTheme>(theme => ({ + fullHeightDialog: { + height: 'calc(100% - 64px)', + }, + root: { + display: 'flex', + flexGrow: 1, + width: '100%', + height: '100%', + }, + tabs: { + borderRight: `1px solid ${theme.palette.divider}`, + flexShrink: 0, + }, + tabContents: { + flexGrow: 1, + overflowX: 'auto', + }, + title: { + color: theme.palette.text.primary, + wordBreak: 'break-word', + fontSize: theme.typography.h3.fontSize, + marginBottom: 0, + }, + type: { + textTransform: 'uppercase', + fontSize: 11, + opacity: 0.8, + marginBottom: theme.spacing(1), + color: theme.palette.text.primary, + }, +})); + +function TabPanel(props: { + children?: React.ReactNode; + index: number; + value: number; +}) { + const { children, value, index, ...other } = props; + const classes = useStyles(); + return ( + <div + role="tabpanel" + hidden={value !== index} + id={`vertical-tabpanel-${index}`} + aria-labelledby={`vertical-tab-${index}`} + className={classes.tabContents} + {...other} + > + {value === index && ( + <Box pl={3} pr={3}> + {children} + </Box> + )} + </div> + ); +} + +function a11yProps(index: number) { + return { + id: `vertical-tab-${index}`, + 'aria-controls': `vertical-tabpanel-${index}`, + }; +} + +/** + * A dialog that lets users inspect the API definition. + * + * @public + */ +export function ApiDefinitionDialog(props: { + open: boolean; + entity: ApiEntity; + onClose: () => void; +}) { + const { open, entity, onClose } = props; + const [activeTab, setActiveTab] = React.useState(0); + const classes = useStyles(); + + useEffect(() => { + setActiveTab(0); + }, [open]); + + const config = useApi(apiDocsConfigRef); + const definitionWidget = config.getApiDefinitionWidget(entity); + + let tabIndex = 0; + let tabPanelIndex = 0; + + return ( + <Dialog + fullWidth + maxWidth="xl" + open={open} + onClose={onClose} + aria-labelledby="api-definition-dialog-title" + PaperProps={{ className: classes.fullHeightDialog }} + > + <DialogTitle id="api-definition-dialog-title" disableTypography> + <Typography className={classes.type}> + API - {definitionWidget?.title ?? 'Raw'} + </Typography> + <Typography className={classes.title} variant="h1"> + {entity.metadata.title ?? entity.metadata.name} + </Typography> + </DialogTitle> + <DialogContent dividers className={classes.root}> + <Tabs + orientation="vertical" + variant="scrollable" + value={activeTab} + onChange={(_, newValue) => setActiveTab(newValue)} + aria-label="API definition options" + className={classes.tabs} + > + {definitionWidget ? ( + <Tab label={definitionWidget.title} {...a11yProps(tabIndex++)} /> + ) : null} + <Tab label="Raw" {...a11yProps(tabIndex++)} /> + </Tabs> + + {definitionWidget ? ( + <TabPanel value={activeTab} index={tabPanelIndex++}> + {definitionWidget.component(entity.spec.definition)} + </TabPanel> + ) : null} + <TabPanel value={activeTab} index={tabPanelIndex++}> + <PlainApiDefinitionWidget + definition={entity.spec.definition} + language={definitionWidget?.rawLanguage ?? entity.spec.type} + /> + </TabPanel> + </DialogContent> + <DialogActions> + <Button onClick={onClose} color="primary"> + Close + </Button> + </DialogActions> + </Dialog> + ); +} diff --git a/plugins/api-docs/src/components/ApiDefinitionDialog/index.ts b/plugins/api-docs/src/components/ApiDefinitionDialog/index.ts new file mode 100644 index 0000000000..28e3388bcc --- /dev/null +++ b/plugins/api-docs/src/components/ApiDefinitionDialog/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ApiDefinitionDialog } from './ApiDefinitionDialog'; diff --git a/plugins/api-docs/src/components/ApisCards/presets.tsx b/plugins/api-docs/src/components/ApisCards/presets.tsx index 4a0a0f429e..b1ef39c113 100644 --- a/plugins/api-docs/src/components/ApisCards/presets.tsx +++ b/plugins/api-docs/src/components/ApisCards/presets.tsx @@ -15,10 +15,13 @@ */ import { ApiEntity } from '@backstage/catalog-model'; -import { EntityTable } from '@backstage/plugin-catalog-react'; -import React from 'react'; -import { ApiTypeTitle } from '../ApiDefinitionCard'; import { TableColumn } from '@backstage/core-components'; +import { EntityTable } from '@backstage/plugin-catalog-react'; +import ExtensionIcon from '@material-ui/icons/Extension'; +import { ToggleButton } from '@material-ui/lab'; +import React, { useState } from 'react'; +import { ApiTypeTitle } from '../ApiDefinitionCard'; +import { ApiDefinitionDialog } from '../ApiDefinitionDialog'; export function createSpecApiTypeColumn(): TableColumn<ApiEntity> { return { @@ -28,9 +31,29 @@ export function createSpecApiTypeColumn(): TableColumn<ApiEntity> { }; } -// TODO: This could be moved to plugin-catalog-react if we wouldn't have a -// special createSpecApiTypeColumn. But this is required to use ApiTypeTitle to -// resolve the display name of an entity. Is the display name really worth it? +const ApiDefinitionButton = ({ apiEntity }: { apiEntity: ApiEntity }) => { + const [dialogOpen, setDialogOpen] = useState(false); + + return ( + <> + <ToggleButton onClick={() => setDialogOpen(!dialogOpen)}> + <ExtensionIcon /> + </ToggleButton> + <ApiDefinitionDialog + entity={apiEntity} + open={dialogOpen} + onClose={() => setDialogOpen(false)} + /> + </> + ); +}; + +function createApiDefinitionColumn(): TableColumn<ApiEntity> { + return { + title: 'API Definition', + render: entity => <ApiDefinitionButton apiEntity={entity} />, + }; +} export const apiEntityColumns: TableColumn<ApiEntity>[] = [ EntityTable.columns.createEntityRefColumn({ defaultKind: 'API' }), @@ -39,4 +62,5 @@ export const apiEntityColumns: TableColumn<ApiEntity>[] = [ createSpecApiTypeColumn(), EntityTable.columns.createSpecLifecycleColumn(), EntityTable.columns.createMetadataDescriptionColumn(), + createApiDefinitionColumn(), ]; diff --git a/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.test.tsx b/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.test.tsx new file mode 100644 index 0000000000..d7e6951e2e --- /dev/null +++ b/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { TrpcApiDefinitionWidget } from './TrpcApiDefinitionWidget'; + +describe('<TrpcApiDefinitionWidget />', () => { + it('renders plain text', async () => { + const { getAllByText } = await renderInTestApp( + <TrpcApiDefinitionWidget definition="Hello World" />, + ); + + expect( + getAllByText((_text, element) => element?.textContent === 'Hello World') + .length, + ).toBeGreaterThan(0); + }); +}); diff --git a/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.tsx b/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.tsx new file mode 100644 index 0000000000..af73335dbd --- /dev/null +++ b/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { CodeSnippet } from '@backstage/core-components'; +import { useTheme } from '@material-ui/core/styles'; +import { BackstageTheme } from '@backstage/theme'; + +/** @public */ +export type TrpcApiDefinitionWidgetProps = { + definition: string; +}; + +/** @public */ +export const TrpcApiDefinitionWidget = ( + props: TrpcApiDefinitionWidgetProps, +) => { + const { definition } = props; + const theme = useTheme<BackstageTheme>(); + return ( + <CodeSnippet + customStyle={{ backgroundColor: theme.palette.background.default }} + text={definition} + language="typescript" + showCopyCodeButton + /> + ); +}; diff --git a/plugins/api-docs/src/components/TrpcDefinitionWidget/index.ts b/plugins/api-docs/src/components/TrpcDefinitionWidget/index.ts new file mode 100644 index 0000000000..604adb06f3 --- /dev/null +++ b/plugins/api-docs/src/components/TrpcDefinitionWidget/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + TrpcApiDefinitionWidget, + type TrpcApiDefinitionWidgetProps, +} from './TrpcApiDefinitionWidget'; diff --git a/plugins/api-docs/src/components/index.ts b/plugins/api-docs/src/components/index.ts index ea40076af4..12297a91a1 100644 --- a/plugins/api-docs/src/components/index.ts +++ b/plugins/api-docs/src/components/index.ts @@ -16,9 +16,11 @@ export * from './ApiExplorerPage'; export * from './ApiDefinitionCard'; +export * from './ApiDefinitionDialog'; export * from './ApisCards'; export * from './AsyncApiDefinitionWidget'; export * from './ComponentsCards'; export * from './GraphQlDefinitionWidget'; export * from './OpenApiDefinitionWidget'; export * from './PlainApiDefinitionWidget'; +export * from './TrpcDefinitionWidget'; diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md index a123d9c7ed..b3d1d90f53 100644 --- a/plugins/apollo-explorer/CHANGELOG.md +++ b/plugins/apollo-explorer/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-apollo-explorer +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.1.8 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.1.8-next.0 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.1.7 ### Patch Changes diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json index 60fbb7fa7b..8251dac9b9 100644 --- a/plugins/apollo-explorer/package.json +++ b/plugins/apollo-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apollo-explorer", - "version": "0.1.7", + "version": "0.1.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -26,7 +26,7 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.9.13", + "@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", @@ -45,7 +45,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 88c94b2be2..ef9392a626 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-app-backend +## 0.3.43-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + +## 0.3.42 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + +## 0.3.42-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + +## 0.3.42-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + +## 0.3.42-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.3.40 ### Patch Changes diff --git a/plugins/app-backend/alpha-api-report.md b/plugins/app-backend/alpha-api-report.md new file mode 100644 index 0000000000..20a2902557 --- /dev/null +++ b/plugins/app-backend/alpha-api-report.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/plugin-app-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import express from 'express'; + +// @alpha +export const appPlugin: (options: AppPluginOptions) => BackendFeature; + +// @alpha (undocumented) +export type AppPluginOptions = { + appPackageName?: string; + staticFallbackHandler?: express.Handler; + disableConfigInjection?: boolean; + disableStaticFallbackCache?: boolean; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/app-backend/api-report.md b/plugins/app-backend/api-report.md index b52132ef51..0383129823 100644 --- a/plugins/app-backend/api-report.md +++ b/plugins/app-backend/api-report.md @@ -3,23 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; -// @alpha -export const appPlugin: (options: AppPluginOptions) => BackendFeature; - -// @alpha (undocumented) -export type AppPluginOptions = { - appPackageName?: string; - staticFallbackHandler?: express.Handler; - disableConfigInjection?: boolean; - disableStaticFallbackCache?: boolean; -}; - // @public (undocumented) export function createRouter(options: RouterOptions): Promise<express.Router>; diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 3e729baf27..203faaa517 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.40", + "version": "0.3.43-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -57,13 +69,12 @@ "@backstage/types": "workspace:^", "@types/supertest": "^2.0.8", "mock-fs": "^5.1.0", - "msw": "^0.49.0", + "msw": "^1.0.0", "node-fetch": "^2.6.7", "supertest": "^6.1.3" }, "files": [ "dist", - "alpha", "migrations/**/*.{js,d.ts}", "static" ] diff --git a/plugins/app-backend/src/alpha.ts b/plugins/app-backend/src/alpha.ts new file mode 100644 index 0000000000..cc8276b853 --- /dev/null +++ b/plugins/app-backend/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { appPlugin } from './service/appPlugin'; +export type { AppPluginOptions } from './service/appPlugin'; diff --git a/plugins/app-backend/src/index.ts b/plugins/app-backend/src/index.ts index 167dc041fc..c91416eac1 100644 --- a/plugins/app-backend/src/index.ts +++ b/plugins/app-backend/src/index.ts @@ -21,5 +21,3 @@ */ export * from './service/router'; -export { appPlugin } from './service/appPlugin'; -export type { AppPluginOptions } from './service/appPlugin'; diff --git a/plugins/app-backend/src/service/appPlugin.ts b/plugins/app-backend/src/service/appPlugin.ts index b12a8d6877..a29f8fd5a8 100644 --- a/plugins/app-backend/src/service/appPlugin.ts +++ b/plugins/app-backend/src/service/appPlugin.ts @@ -71,7 +71,7 @@ export type AppPluginOptions = { * @alpha */ export const appPlugin = createBackendPlugin((options: AppPluginOptions) => ({ - id: 'app', + pluginId: 'app', register(env) { env.registerInit({ deps: { diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 2bb5a4a695..5e2b975fc1 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-auth-backend +## 0.18.1-next.0 + +### Patch Changes + +- 529de8c421: Added authentication provider for Azure Easy Authentication. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + +## 0.18.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + +## 0.17.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.17.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## 0.17.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + ## 0.17.3 ### Patch Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 5caea64b33..7515083c09 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -7,7 +7,7 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { BackstageSignInResult } from '@backstage/plugin-auth-node'; -import { CacheClient } from '@backstage/backend-plugin-api'; +import { CacheService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; @@ -132,6 +132,19 @@ export type BitbucketPassportProfile = Profile & { }; }; +// @public (undocumented) +export type BitbucketServerOAuthResult = { + fullProfile: Profile; + params: { + scope: string; + access_token?: string; + token_type?: string; + expires_in?: number; + }; + accessToken: string; + refreshToken?: string; +}; + // @public export class CatalogIdentityClient { constructor(options: { catalogApi: CatalogApi; tokenManager: TokenManager }); @@ -482,13 +495,30 @@ export const providers: Readonly<{ userIdMatchingUserEntityAnnotation(): SignInResolver<OAuthResult>; }>; }>; + bitbucketServer: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler<BitbucketServerOAuthResult> | undefined; + signIn?: + | { + resolver: SignInResolver<BitbucketServerOAuthResult>; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + emailMatchingUserEntityProfileEmail: () => SignInResolver<BitbucketServerOAuthResult>; + }>; + }>; cfAccess: Readonly<{ create: (options: { authHandler?: AuthHandler<CloudflareAccessResult> | undefined; signIn: { resolver: SignInResolver<CloudflareAccessResult>; }; - cache?: CacheClient | undefined; + cache?: CacheService | undefined; }) => AuthProviderFactory; resolvers: Readonly<{ emailMatchingUserEntityProfileEmail: () => SignInResolver<unknown>; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index e998c866dc..a65377276e 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.17.3", + "version": "0.18.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -78,6 +78,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/test-utils": "workspace:^", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", @@ -85,11 +86,11 @@ "@types/passport-auth0": "^1.0.5", "@types/passport-github2": "^1.2.4", "@types/passport-google-oauth20": "^2.0.3", - "@types/passport-microsoft": "^0.0.0", + "@types/passport-microsoft": "^1.0.0", "@types/passport-saml": "^1.1.3", "@types/passport-strategy": "^0.2.35", "@types/xml2js": "^0.4.7", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/auth-backend/src/providers/azure-easyauth/index.ts b/plugins/auth-backend/src/providers/azure-easyauth/index.ts new file mode 100644 index 0000000000..698092d73f --- /dev/null +++ b/plugins/auth-backend/src/providers/azure-easyauth/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { easyAuth } from './provider'; +export type { EasyAuthResponse } from './provider'; diff --git a/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts b/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts new file mode 100644 index 0000000000..b521ddeb10 --- /dev/null +++ b/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts @@ -0,0 +1,292 @@ +/* + * 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 { AuthHandler, AuthResolverContext } from '../types'; +import { makeProfileInfo } from '../../lib/passport'; +import { + easyAuth, + ACCESS_TOKEN_HEADER, + EasyAuthAuthProvider, + EasyAuthResult, + ID_TOKEN_HEADER, +} from './provider'; +import { Request, Response } from 'express'; +import { SignJWT, JWTPayload, errors as JoseErrors } from 'jose'; +import { randomBytes } from 'crypto'; + +const jwtSecret = randomBytes(48); + +async function buildJwt(claims: JWTPayload) { + return await new SignJWT(claims) + .setProtectedHeader({ alg: 'HS256' }) + .sign(jwtSecret); +} + +const backstageIdentityTokenClaims = { + sub: 'user:default/alice', + ent: ['user:default/alice'], +}; + +describe('EasyAuthAuthProvider', () => { + const authHandler: AuthHandler<EasyAuthResult> = async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }); + const resolverContext: AuthResolverContext = {} as AuthResolverContext; + async function signInResolver() { + return { + id: 'user.name', + token: await buildJwt(backstageIdentityTokenClaims), + }; + } + + const provider = new EasyAuthAuthProvider({ + authHandler, + signInResolver, + resolverContext, + }); + + function mockRequest(headers?: Record<string, string>) { + return { + header: (name: string) => headers?.[name], + } as unknown as Request; + } + + describe('should succeed when', () => { + it('id_token is valid and identity is resolved successfully', async () => { + const claims = { + ver: '2.0', + oid: 'c43063d4-0650-4f3e-ba6b-307473d24dfd', + name: 'Alice Bob', + email: 'alice@bob.com', + preferred_username: 'Another name', + }; + const response = { + end: jest.fn(), + header: () => jest.fn(), + json: jest.fn(), + status: jest.fn(), + } as unknown as Response; + + const request = mockRequest({ + [ID_TOKEN_HEADER]: await buildJwt(claims), + }); + await provider.refresh(request, response); + + expect(response.json).toHaveBeenCalledWith({ + backstageIdentity: { + id: 'user.name', + token: await buildJwt(backstageIdentityTokenClaims), + identity: { + ownershipEntityRefs: ['user:default/alice'], + type: 'user', + userEntityRef: 'user:default/alice', + }, + }, + profile: { + displayName: claims.name, + email: claims.email, + picture: undefined, + }, + providerInfo: { + accessToken: undefined, + }, + }); + }); + + it('valid id_token and access_token provided', async () => { + const claims = { + ver: '2.0', + oid: 'c43063d4-0650-4f3e-ba6b-307473d24dfd', + name: 'Alice Bob', + email: 'alice@bob.com', + preferred_username: 'Another name', + }; + const response = { + end: jest.fn(), + header: () => jest.fn(), + json: jest.fn(), + status: jest.fn(), + } as unknown as Response; + + const request = mockRequest({ + [ID_TOKEN_HEADER]: await buildJwt(claims), + [ACCESS_TOKEN_HEADER]: 'ACCESS_TOKEN', + }); + await provider.refresh(request, response); + + expect(response.json).toHaveBeenCalledWith( + expect.objectContaining({ + providerInfo: { + accessToken: 'ACCESS_TOKEN', + }, + }), + ); + }); + }); + + describe('should fail when', () => { + const response = {} as Response; + + it('Access token is missing', async () => { + const request = mockRequest(); + + await expect(provider.refresh(request, response)).rejects.toThrow( + 'Missing x-ms-token-aad-id-token header', + ); + }); + + it('id token is invalid', async () => { + const request = mockRequest({ + [ID_TOKEN_HEADER]: 'not-a-jwt', + }); + + await expect(provider.refresh(request, response)).rejects.toThrow( + JoseErrors.JWTInvalid, + ); + }); + + it('id token is v1', async () => { + const request = mockRequest({ + [ID_TOKEN_HEADER]: await buildJwt({ ver: '1.0' }), + }); + + await expect(provider.refresh(request, response)).rejects.toThrow( + 'id_token is not version 2.0', + ); + }); + + it('SignInResolver rejects', async () => { + const request = mockRequest({ + [ID_TOKEN_HEADER]: await buildJwt({ ver: '2.0' }), + }); + + const rejectProvider = new EasyAuthAuthProvider({ + authHandler, + signInResolver: async () => { + throw new Error('REJECTED!!'); + }, + resolverContext, + }); + + await expect(rejectProvider.refresh(request, response)).rejects.toThrow( + 'REJECTED!!', + ); + }); + + it('AuthHanlder rejects', async () => { + const request = mockRequest({ + [ID_TOKEN_HEADER]: await buildJwt({ ver: '2.0' }), + }); + + const rejectProvider = new EasyAuthAuthProvider({ + authHandler: async () => { + throw new Error('OBJECTION!!'); + }, + signInResolver, + resolverContext, + }); + + await expect(rejectProvider.refresh(request, response)).rejects.toThrow( + 'OBJECTION!!', + ); + }); + }); +}); + +describe('easyAuth factory', () => { + const env = process.env; + beforeEach(() => { + jest.resetModules(); + process.env = { ...env }; + }); + + afterEach(() => { + process.env = env; + }); + + it('should fail when run outside of Azure App Services', async () => { + const factory = easyAuth.create({ + signIn: { + resolver: jest.fn(), + }, + }); + + expect(() => factory({} as any)).toThrow( + 'Backstage is not running on Azure App Services', + ); + }); + + it('should fail when Azure App Services Auth is not enabled', async () => { + process.env.WEBSITE_SKU = 'Standard'; + process.env.WEBSITE_AUTH_ENABLED = 'False'; + + const factory = easyAuth.create({ + signIn: { + resolver: jest.fn(), + }, + }); + + expect(() => factory({} as any)).toThrow( + 'Azure App Services does not have authentication enabled', + ); + }); + + it('should fail when Azure App Services Auth is not AAD', async () => { + process.env.WEBSITE_SKU = 'Standard'; + process.env.WEBSITE_AUTH_ENABLED = 'True'; + process.env.WEBSITE_AUTH_DEFAULT_PROVIDER = 'Facebook'; + + const factory = easyAuth.create({ + signIn: { + resolver: jest.fn(), + }, + }); + + expect(() => factory({} as any)).toThrow( + 'Authentication provider is not Azure Active Directory', + ); + }); + + it('should fail when Token Store not enabled', async () => { + process.env.WEBSITE_SKU = 'Standard'; + process.env.WEBSITE_AUTH_ENABLED = 'True'; + process.env.WEBSITE_AUTH_DEFAULT_PROVIDER = 'AzureActiveDirectory'; + process.env.WEBSITE_AUTH_TOKEN_STORE = 'False'; + + const factory = easyAuth.create({ + signIn: { + resolver: jest.fn(), + }, + }); + + expect(() => factory({} as any)).toThrow('Token Store is not enabled'); + }); + + it('should return EasyAuthAuthProvider when running in Azure App Services with AAD Auth', async () => { + process.env.WEBSITE_SKU = 'Standard'; + process.env.WEBSITE_AUTH_ENABLED = 'True'; + process.env.WEBSITE_AUTH_DEFAULT_PROVIDER = 'AzureActiveDirectory'; + process.env.WEBSITE_AUTH_TOKEN_STORE = 'True'; + + const factory = easyAuth.create({ + signIn: { + resolver: jest.fn(), + }, + }); + + expect(factory({} as any)).toBeInstanceOf(EasyAuthAuthProvider); + }); +}); diff --git a/plugins/auth-backend/src/providers/azure-easyauth/provider.ts b/plugins/auth-backend/src/providers/azure-easyauth/provider.ts new file mode 100644 index 0000000000..405ca9ed59 --- /dev/null +++ b/plugins/auth-backend/src/providers/azure-easyauth/provider.ts @@ -0,0 +1,192 @@ +/* + * 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 { + AuthHandler, + AuthProviderRouteHandlers, + AuthResolverContext, + AuthResponse, + SignInResolver, +} from '../types'; +import { Request, Response } from 'express'; +import { makeProfileInfo } from '../../lib/passport'; +import { AuthenticationError } from '@backstage/errors'; +import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { Profile } from 'passport'; +import { decodeJwt } from 'jose'; + +export const ID_TOKEN_HEADER = 'x-ms-token-aad-id-token'; +export const ACCESS_TOKEN_HEADER = 'x-ms-token-aad-access-token'; + +type Options = { + authHandler: AuthHandler<EasyAuthResult>; + signInResolver: SignInResolver<EasyAuthResult>; + resolverContext: AuthResolverContext; +}; + +export type EasyAuthResult = { + fullProfile: Profile; + accessToken?: string; +}; + +export type EasyAuthResponse = AuthResponse<{}>; + +export class EasyAuthAuthProvider implements AuthProviderRouteHandlers { + private readonly resolverContext: AuthResolverContext; + private readonly authHandler: AuthHandler<EasyAuthResult>; + private readonly signInResolver: SignInResolver<EasyAuthResult>; + + constructor(options: Options) { + this.authHandler = options.authHandler; + this.signInResolver = options.signInResolver; + this.resolverContext = options.resolverContext; + } + + frameHandler(): Promise<void> { + return Promise.resolve(undefined); + } + + async refresh(req: Request, res: Response): Promise<void> { + const result = await this.getResult(req); + const response = await this.handleResult(result); + res.json(response); + } + + start(): Promise<void> { + return Promise.resolve(undefined); + } + + private async getResult(req: Request): Promise<EasyAuthResult> { + const idToken = req.header(ID_TOKEN_HEADER); + const accessToken = req.header(ACCESS_TOKEN_HEADER); + if (idToken === undefined) { + throw new AuthenticationError(`Missing ${ID_TOKEN_HEADER} header`); + } + + return { + fullProfile: this.idTokenToProfile(idToken), + accessToken: accessToken, + }; + } + + private idTokenToProfile(idToken: string) { + const claims = decodeJwt(idToken); + + if (claims.ver !== '2.0') { + throw new Error('id_token is not version 2.0 '); + } + + return { + id: claims.oid, + displayName: claims.name, + provider: 'easyauth', + emails: [{ value: claims.email }], + username: claims.preferred_username, + } as Profile; + } + + private async handleResult( + result: EasyAuthResult, + ): Promise<EasyAuthResponse> { + const { profile } = await this.authHandler(result, this.resolverContext); + + const backstageIdentity = await this.signInResolver( + { + result, + profile, + }, + this.resolverContext, + ); + + return { + providerInfo: { + accessToken: result.accessToken, + }, + backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity), + profile, + }; + } +} + +/** + * Auth provider integration for Azure EasyAuth + * + * @public + */ +export const easyAuth = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler<EasyAuthResult>; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver<EasyAuthResult>; + }; + }) { + return ({ resolverContext }) => { + validateAppServiceConfiguration(process.env); + + if (options?.signIn.resolver === undefined) { + throw new Error( + 'SignInResolver is required to use this authentication provider', + ); + } + + const authHandler = + options.authHandler ?? + (async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + })); + + return new EasyAuthAuthProvider({ + signInResolver: options.signIn.resolver, + authHandler, + resolverContext, + }); + }; + }, +}); + +function validateAppServiceConfiguration(env: NodeJS.ProcessEnv) { + // Based on https://github.com/AzureAD/microsoft-identity-web/blob/f7403779d1a91f4a3fec0ed0993bd82f50f299e1/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationInformation.cs#L38-L59 + // + // It's critical to validate we're really running in a correctly configured Azure App Services, + // As we rely on App Services to manage & validate the ID and Access Token headers + // Without that, this users can be trivially impersonated. + if (env.WEBSITE_SKU === undefined) { + throw new Error('Backstage is not running on Azure App Services'); + } + if (env.WEBSITE_AUTH_ENABLED?.toLowerCase() !== 'true') { + throw new Error('Azure App Services does not have authentication enabled'); + } + if ( + env.WEBSITE_AUTH_DEFAULT_PROVIDER?.toLowerCase() !== 'azureactivedirectory' + ) { + throw new Error('Authentication provider is not Azure Active Directory'); + } + if (process.env.WEBSITE_AUTH_TOKEN_STORE?.toLowerCase() !== 'true') { + throw new Error('Token Store is not enabled'); + } +} diff --git a/plugins/auth-backend/src/providers/bitbucketServer/index.ts b/plugins/auth-backend/src/providers/bitbucketServer/index.ts new file mode 100644 index 0000000000..cef12cd50d --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucketServer/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 { bitbucketServer } from './provider'; +export type { BitbucketServerOAuthResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts new file mode 100644 index 0000000000..fc48cdbe8b --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts @@ -0,0 +1,390 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as helpers from '../../lib/passport/PassportStrategyHelper'; +import { makeProfileInfo } from '../../lib/passport'; +import { AuthResolverContext } from '../types'; +import { + bitbucketServer, + BitbucketServerAuthProvider, + BitbucketServerOAuthResult, +} from './provider'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; + +jest.mock('../../lib/passport/PassportStrategyHelper', () => { + return { + ...jest.requireActual('../../lib/passport/PassportStrategyHelper'), + executeFrameHandlerStrategy: jest.fn(), + executeRefreshTokenStrategy: jest.fn(), + executeFetchUserProfileStrategy: jest.fn(), + }; +}); + +const mockFrameHandler = jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown as jest.MockedFunction< + () => Promise<{ + result: BitbucketServerOAuthResult; + privateInfo: { refreshToken?: string }; + }> +>; + +const passportProfile = { + id: '123', + username: 'john.doe', + provider: 'bitubcketServer', + displayName: 'John Doe', + emails: [{ value: 'john@doe.com' }], + photos: [{ value: 'https://bitbucket.org/user/123/avatar' }], +}; + +const mockHost = 'bitbucket.org'; +const mockBaseUrl = `https://${mockHost}`; + +const whoAmIHandler = (options?: { fail?: boolean; value?: string }) => + rest.get( + `${mockBaseUrl}/plugins/servlet/applinks/whoami`, + (_req, res, ctx) => { + if (options?.fail) { + res.networkError('error'); + } + return res( + ctx.status(200), + ctx.set('X-Ausername', options?.value ?? passportProfile.username), + ); + }, + ); + +const getUserHandler = (options?: { + fail?: boolean; + status?: number; + avatarUrl?: string; + noDisplayName?: boolean; + noUserName?: boolean; +}) => + rest.get( + `${mockBaseUrl}/rest/api/latest/users/${passportProfile.username}`, + (_req, res, ctx) => { + if (options?.fail) { + res.networkError('error'); + } + return res( + ctx.status(options?.status ?? 200), + ctx.json({ + name: options?.noUserName ? undefined : 'john.doe', + emailAddress: 'john@doe.com', + id: 123, + displayName: options?.noDisplayName ? undefined : 'John Doe', + active: true, + slug: 'john.doe', + type: 'NORMAL', + links: { + self: [ + { + href: 'https://bitbucket.org/users/john.doe', + }, + ], + }, + avatarUrl: options?.avatarUrl ?? '/user/123/avatar', + }), + ); + }, + ); + +describe('BitbucketServerAuthProvider', () => { + const provider = new BitbucketServerAuthProvider({ + resolverContext: { + signInWithCatalogUser: jest.fn(info => { + return { + token: `token-for-user:${info.filter['spec.profile.email']}`, + }; + }), + } as unknown as AuthResolverContext, + signInResolver: + bitbucketServer.resolvers.emailMatchingUserEntityProfileEmail(), + authHandler: async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }), + callbackUrl: 'mock', + clientId: 'mock', + clientSecret: 'mock', + host: mockHost, + authorizationUrl: 'mock', + tokenUrl: 'mock', + }); + + describe('when transforming to type OAuthResponse', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + it('should map to a valid response', async () => { + server.use(whoAmIHandler(), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + + const expected = { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'John Doe', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }; + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); + }); + + it('should throw if whoami fails', async () => { + server.use(whoAmIHandler({ fail: true }), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + `Failed to retrieve the username of the logged in user`, + ); + }); + + it('should throw if whoami returns an invalid response', async () => { + server.use(whoAmIHandler({ value: '' }), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + `Failed to retrieve the username of the logged in user`, + ); + }); + + it('should throw if get user fails', async () => { + server.use(whoAmIHandler(), getUserHandler({ fail: true })); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + `Failed to retrieve the user '${passportProfile.username}'`, + ); + }); + + it('should throw if get user is not ok', async () => { + server.use(whoAmIHandler(), getUserHandler({ status: 500 })); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + `Failed to retrieve the user '${passportProfile.username}'`, + ); + }); + + it('should not set an avatar url if not given', async () => { + server.use(whoAmIHandler(), getUserHandler({ avatarUrl: '' })); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + + const expected = { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'John Doe', + }, + }; + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); + }); + + it('should fallback to the username if no displayName is given', async () => { + server.use(whoAmIHandler(), getUserHandler({ noDisplayName: true })); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + + const expected = { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'john.doe', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }; + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); + }); + + it('should fallback to the user id if no name is given', async () => { + server.use( + whoAmIHandler(), + getUserHandler({ noDisplayName: true, noUserName: true }), + ); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + + const expected = { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: '123', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }; + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); + }); + }); + + describe('when authenticating', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + it('should forward the refresh token', async () => { + server.use(whoAmIHandler(), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: { refreshToken: 'refresh-token' }, + }); + + const response = await provider.handler({} as any); + + const expected = { + response: { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'John Doe', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }, + refreshToken: 'refresh-token', + }; + + expect(response).toEqual(expected); + }); + + it('should forward a new refresh token on refresh', async () => { + server.use(whoAmIHandler(), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + const mockRefreshToken = jest.spyOn( + helpers, + 'executeRefreshTokenStrategy', + ) as unknown as jest.MockedFunction<() => Promise<{}>>; + mockRefreshToken.mockResolvedValueOnce({ + accessToken, + refreshToken: 'dont-forget-to-send-refresh', + params, + }); + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: { refreshToken: 'refresh-token' }, + }); + + const expected = { + response: { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'John Doe', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }, + refreshToken: 'dont-forget-to-send-refresh', + }; + const response = await provider.refresh({ scope: 'REPO_WRITE' } as any); + + expect(response).toEqual(expected); + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts new file mode 100644 index 0000000000..fb8dd3d5b8 --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts @@ -0,0 +1,305 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + encodeState, + OAuthAdapter, + OAuthEnvironmentHandler, + OAuthHandlers, + OAuthProviderOptions, + OAuthRefreshRequest, + OAuthResponse, + OAuthStartRequest, +} from '../../lib/oauth'; +import { Strategy as OAuth2Strategy, VerifyCallback } from 'passport-oauth2'; +import { + executeFetchUserProfileStrategy, + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, +} from '../../lib/passport'; +import { + AuthHandler, + AuthResolverContext, + OAuthStartResponse, + SignInResolver, +} from '../types'; +import express from 'express'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { Profile as PassportProfile } from 'passport'; +import { commonByEmailResolver } from '../resolvers'; +import fetch from 'node-fetch'; + +type PrivateInfo = { + refreshToken: string; +}; + +/** @public */ +export type BitbucketServerOAuthResult = { + fullProfile: PassportProfile; + params: { + scope: string; + access_token?: string; + token_type?: string; + expires_in?: number; + }; + accessToken: string; + refreshToken?: string; +}; + +export type BitbucketServerAuthProviderOptions = OAuthProviderOptions & { + host: string; + authorizationUrl: string; + tokenUrl: string; + authHandler: AuthHandler<BitbucketServerOAuthResult>; + signInResolver?: SignInResolver<BitbucketServerOAuthResult>; + resolverContext: AuthResolverContext; +}; + +export class BitbucketServerAuthProvider implements OAuthHandlers { + private readonly signInResolver?: SignInResolver<BitbucketServerOAuthResult>; + private readonly authHandler: AuthHandler<BitbucketServerOAuthResult>; + private readonly resolverContext: AuthResolverContext; + private readonly strategy: OAuth2Strategy; + private readonly host: string; + + constructor(options: BitbucketServerAuthProviderOptions) { + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.resolverContext = options.resolverContext; + this.strategy = new OAuth2Strategy( + { + authorizationURL: options.authorizationUrl, + tokenURL: options.tokenUrl, + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: VerifyCallback, + ) => { + done(undefined, { fullProfile, params, accessToken }, { refreshToken }); + }, + ); + this.host = options.host; + } + + async start(req: OAuthStartRequest): Promise<OAuthStartResponse> { + return await executeRedirectStrategy(req, this.strategy, { + accessType: 'offline', + prompt: 'consent', + scope: req.scope, + state: encodeState(req.state), + }); + } + + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken?: string }> { + const { result, privateInfo } = await executeFrameHandlerStrategy< + BitbucketServerOAuthResult, + PrivateInfo + >(req, this.strategy); + + return { + response: await this.handleResult(result), + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh( + req: OAuthRefreshRequest, + ): Promise<{ response: OAuthResponse; refreshToken?: string }> { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this.strategy, + req.refreshToken, + req.scope, + ); + const fullProfile = await executeFetchUserProfileStrategy( + this.strategy, + accessToken, + ); + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; + } + + private async handleResult( + result: BitbucketServerOAuthResult, + ): Promise<OAuthResponse> { + // The OAuth2 strategy does not return a user profile -> let's fetch it before calling the auth handler + result.fullProfile = await this.fetchProfile(result); + const { profile } = await this.authHandler(result, this.resolverContext); + + let backstageIdentity = undefined; + if (this.signInResolver) { + backstageIdentity = await this.signInResolver( + { result, profile }, + this.resolverContext, + ); + } + + return { + providerInfo: { + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + backstageIdentity, + }; + } + + private async fetchProfile( + result: BitbucketServerOAuthResult, + ): Promise<PassportProfile> { + // Get current user name + let whoAmIResponse; + try { + whoAmIResponse = await fetch( + `https://${this.host}/plugins/servlet/applinks/whoami`, + { + headers: { + Authorization: `Bearer ${result.accessToken}`, + }, + }, + ); + } catch (e) { + throw new Error(`Failed to retrieve the username of the logged in user`); + } + + // A response.ok check here would be worthless as the Bitbucket API always returns 200 OK for this call + const username = whoAmIResponse.headers.get('X-Ausername'); + if (!username) { + throw new Error(`Failed to retrieve the username of the logged in user`); + } + + let userResponse; + try { + userResponse = await fetch( + `https://${this.host}/rest/api/latest/users/${username}?avatarSize=256`, + { + headers: { + Authorization: `Bearer ${result.accessToken}`, + }, + }, + ); + } catch (e) { + throw new Error(`Failed to retrieve the user '${username}'`); + } + + if (!userResponse.ok) { + throw new Error(`Failed to retrieve the user '${username}'`); + } + + const user = (await userResponse.json()) as any; + + const passportProfile = { + provider: 'bitbucketServer', + id: user.id.toString(), + displayName: user.displayName, + username: user.name, + emails: [ + { + value: user.emailAddress, + }, + ], + } as PassportProfile; + + if (user.avatarUrl) { + passportProfile.photos = [ + { value: `https://${this.host}${user.avatarUrl}` }, + ]; + } + + return passportProfile; + } +} + +export const bitbucketServer = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler<BitbucketServerOAuthResult>; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver<BitbucketServerOAuthResult>; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const host = envConfig.getString('host'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://${host}/rest/oauth2/latest/authorize`; + const tokenUrl = `https://${host}/rest/oauth2/latest/token`; + + const authHandler: AuthHandler<BitbucketServerOAuthResult> = + options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }); + + const provider = new BitbucketServerAuthProvider({ + callbackUrl, + clientId, + clientSecret, + host, + authorizationUrl, + tokenUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + providerId, + callbackUrl, + }); + }); + }, + resolvers: { + /** + * Looks up the user by matching their email to the entity email. + */ + emailMatchingUserEntityProfileEmail: + (): SignInResolver<BitbucketServerOAuthResult> => commonByEmailResolver, + }, +}); diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts index 6653bc38cb..1abca4bdf4 100644 --- a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts +++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts @@ -83,6 +83,7 @@ const mockCacheClient = { get: jest.fn(), set: jest.fn(), delete: jest.fn(), + withOptions: jest.fn(), }; jest.mock('jose'); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 67d3a62990..a922065fd8 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -19,6 +19,7 @@ export type { BitbucketOAuthResult, BitbucketPassportProfile, } from './bitbucket'; +export type { BitbucketServerOAuthResult } from './bitbucketServer'; export type { CloudflareAccessClaims, CloudflareAccessGroup, diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index b2795c25f0..aa630b7481 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -31,6 +31,7 @@ import { okta } from './okta'; import { onelogin } from './onelogin'; import { saml } from './saml'; import { AuthProviderFactory } from './types'; +import { bitbucketServer } from './bitbucketServer'; /** * All built-in auth provider integrations. @@ -42,6 +43,7 @@ export const providers = Object.freeze({ auth0, awsAlb, bitbucket, + bitbucketServer, cfAccess, gcpIap, github, @@ -76,5 +78,6 @@ export const defaultAuthProviderFactories: { onelogin: onelogin.create(), awsalb: awsAlb.create(), bitbucket: bitbucket.create(), + bitbucketServer: bitbucketServer.create(), atlassian: atlassian.create(), }; diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index ecb0448c17..9f0609bd7a 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-auth-node +## 0.2.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.2.9 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index efefb888aa..fcb0ed8e48 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.2.9", + "version": "0.2.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "lodash": "^4.17.21", - "msw": "^0.49.0", + "msw": "^1.0.0", "uuid": "^8.0.0" }, "files": [ diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index 27d25f0a25..2f28ddd584 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-azure-devops-backend +## 0.3.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.21 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- cc926a59bd: Fixed a bug where the azure devops host in URLs on the readme card was being URL encoded, breaking hosts with ports. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.21-next.1 + +### Patch Changes + +- cc926a59bd: Fixed a bug where the azure devops host in URLs on the readme card was being URL encoded, breaking hosts with ports. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.3.19 ### Patch Changes diff --git a/plugins/azure-devops-backend/README.md b/plugins/azure-devops-backend/README.md index 301ae5b2b9..ba2a847c0a 100644 --- a/plugins/azure-devops-backend/README.md +++ b/plugins/azure-devops-backend/README.md @@ -30,9 +30,8 @@ Here's how to get the backend up and running: 1. First we need to add the `@backstage/plugin-azure-devops-backend` package to your backend: ```sh - # From the Backstage root directory - cd packages/backend - yarn add @backstage/plugin-azure-devops-backend + # From your Backstage root directory + yarn add --cwd packages/backend @backstage/plugin-azure-devops-backend ``` 2. Then we will create a new file named `packages/backend/src/plugins/azure-devops.ts`, and add the diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index ead3a03f15..a8baac3673 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.19", + "version": "0.3.22-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,7 +37,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 24c13020b6..87ded2f0e9 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -434,7 +434,7 @@ export function mappedRepoBuild(build: Build): RepoBuild { queueTime: build.queueTime?.toISOString(), startTime: build.startTime?.toISOString(), finishTime: build.finishTime?.toISOString(), - source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`, + source: `${build.sourceBranch} (${build.sourceVersion?.slice(0, 8)})`, uniqueName: build.requestedFor?.uniqueName ?? 'N/A', }; } @@ -489,7 +489,7 @@ export function mappedBuildRun(build: Build): BuildRun { queueTime: build.queueTime?.toISOString(), startTime: build.startTime?.toISOString(), finishTime: build.finishTime?.toISOString(), - source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`, + source: `${build.sourceBranch} (${build.sourceVersion?.slice(0, 8)})`, uniqueName: build.requestedFor?.uniqueName ?? 'N/A', }; } diff --git a/plugins/azure-devops-backend/src/utils/azure-devops-utils.test.ts b/plugins/azure-devops-backend/src/utils/azure-devops-utils.test.ts index 4b912a8340..427980be14 100644 --- a/plugins/azure-devops-backend/src/utils/azure-devops-utils.test.ts +++ b/plugins/azure-devops-backend/src/utils/azure-devops-utils.test.ts @@ -27,6 +27,7 @@ import { getAvatarUrl, getPullRequestLink, replaceReadme, + buildEncodedUrl, } from './azure-devops-utils'; import { GitPullRequest } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { UrlReader } from '@backstage/backend-common'; @@ -278,3 +279,19 @@ describe('replaceReadme', () => { expect(expected).toBe(result); }); }); + +describe('buildEncodedUrl', () => { + it('should not encode the colon between host and port', async () => { + const result = await buildEncodedUrl( + 'tfs.myorg.com:8443', + 'org', + 'project', + 'repo', + 'path', + ); + + expect(result).toBe( + 'https://tfs.myorg.com:8443/org/project/_git/repo?path=path', + ); + }); +}); diff --git a/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts b/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts index 0aafb54a0a..aa946e7a74 100644 --- a/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts +++ b/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts @@ -242,12 +242,11 @@ export function buildEncodedUrl( repo: string, path: string, ): string { - const encodedHost = encodeURIComponent(host); const encodedOrg = encodeURIComponent(org); const encodedProject = encodeURIComponent(project); const encodedRepo = encodeURIComponent(repo); const encodedPath = encodeURIComponent(path); - return `https://${encodedHost}/${encodedOrg}/${encodedProject}/_git/${encodedRepo}?path=${encodedPath}`; + return `https://${host}/${encodedOrg}/${encodedProject}/_git/${encodedRepo}?path=${encodedPath}`; } function convertReviewer( diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index db73a8ff13..3df7331474 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-azure-devops +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.2.6 + +### Patch Changes + +- 347b16ecea: Fixed `AssignedToCurrentUsersTeams` & `CreatedByCurrentUsersTeams` filter in `AzurePullRequestsPage` component. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.2.6-next.0 + +### Patch Changes + +- 347b16ecea: Fixed `AssignedToCurrentUsersTeams` & `CreatedByCurrentUsersTeams` filter in `AzurePullRequestsPage` component. +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index a6869af668..4174675f03 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.2.5", + "version": "0.2.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -56,7 +56,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/azure-sites-backend/CHANGELOG.md b/plugins/azure-sites-backend/CHANGELOG.md index 0c99d9cd2b..32fba35d47 100644 --- a/plugins/azure-sites-backend/CHANGELOG.md +++ b/plugins/azure-sites-backend/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-azure-sites-backend +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.4 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md index 5f19267499..0d44b9a4e4 100644 --- a/plugins/azure-sites-backend/README.md +++ b/plugins/azure-sites-backend/README.md @@ -36,7 +36,7 @@ Here's how to get the backend plugin up and running: 1. First we need to add the `@backstage/plugin-azure-sites-backend` package to your backend: ```sh - # From the Backstage root directory + # From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-azure-sites-backend ``` diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json index c7b25e3d66..50618bb0ad 100644 --- a/plugins/azure-sites-backend/package.json +++ b/plugins/azure-sites-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites-backend", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -48,7 +48,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/azure-sites/CHANGELOG.md b/plugins/azure-sites/CHANGELOG.md index 4bb6ae8d57..e755c154ce 100644 --- a/plugins/azure-sites/CHANGELOG.md +++ b/plugins/azure-sites/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-azure-sites +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.4 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 98a00a09d5: Fixed `AzureSites` plugin start and stop issue #15904 + Fixed `AzureSites` UI load issue #15907 +- 4bdd6bcb3c: Prefer Link over plain anchors +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.4-next.2 + +### Patch Changes + +- 98a00a09d5: Fixed `AzureSites` plugin start and stop issue #15904 + Fixed `AzureSites` UI load issue #15907 +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/azure-sites/README.md b/plugins/azure-sites/README.md index f240dab140..57bf84ccc5 100644 --- a/plugins/azure-sites/README.md +++ b/plugins/azure-sites/README.md @@ -44,10 +44,11 @@ azure.com/microsoft-web-sites: func-testapp ### Install the component -1. Install the plugin in the `packages/app` directory +1. Install the plugin ```sh -yarn add @backstage/plugin-azure-sites +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-azure-sites ``` 2. Add widget component to your Backstage instance: diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json index 5eb38be60c..7d37bf287a 100644 --- a/plugins/azure-sites/package.json +++ b/plugins/azure-sites/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites", - "version": "0.1.3", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -57,7 +57,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts b/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts index fc622e786b..b66a3cbb3f 100644 --- a/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts +++ b/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts @@ -35,7 +35,7 @@ export class AzureSitesApiBackendClient implements AzureSitesApi { } async stop(request: AzureSiteStartStopRequest): Promise<void> { - const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/${ + const url = `${await this.discoveryApi.getBaseUrl('azure-sites')}/${ request.subscription }/${request.resourceGroup}/${request.name}/stop`; const { token: accessToken } = await this.identityApi.getCredentials(); @@ -48,7 +48,7 @@ export class AzureSitesApiBackendClient implements AzureSitesApi { }); } async start(request: AzureSiteStartStopRequest): Promise<void> { - const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/${ + const url = `${await this.discoveryApi.getBaseUrl('azure-sites')}/${ request.subscription }/${request.resourceGroup}/${request.name}/start`; const { token: accessToken } = await this.identityApi.getCredentials(); diff --git a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx index a25d186a48..5b67fe844a 100644 --- a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx +++ b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx @@ -73,9 +73,11 @@ const Kind = ({ value }: { value: Kinds }) => { app: <PublicIcon />, functionapp: <FlashOnIcon />, }; + const kindArray = value.split(','); + const iconValue: Kinds = kindArray.includes('app') ? 'app' : 'functionapp'; return ( <Box display="flex" alignItems="center"> - <Tooltip title={value}>{iconMap[value]}</Tooltip> + <Tooltip title={iconValue}>{iconMap[iconValue]}</Tooltip> </Box> ); }; @@ -162,9 +164,8 @@ const ActionButtons = ({ </MenuItem> )} <MenuItem - component="a" - href={value.logstreamHref} - target="_blank" + component={Link} + to={value.logstreamHref} key="logStream" onClick={handleClose} > diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index 7bb4a154a6..c71299528e 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-badges-backend +## 0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.36 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.36-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + ## 0.1.34 ### Patch Changes diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md index 0ecf41ee85..8ca4b561a2 100644 --- a/plugins/badges-backend/api-report.md +++ b/plugins/badges-backend/api-report.md @@ -79,7 +79,7 @@ export type BadgeSpec = { }; // @public (undocumented) -export type BadgeStyle = typeof BADGE_STYLES[number]; +export type BadgeStyle = (typeof BADGE_STYLES)[number]; // @public (undocumented) export const createDefaultBadgeFactories: () => BadgeFactories; diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index e1c7e4acd0..af9ff60372 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.34", + "version": "0.1.37-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/badges-backend/src/types.ts b/plugins/badges-backend/src/types.ts index eec7063f88..0d778289f9 100644 --- a/plugins/badges-backend/src/types.ts +++ b/plugins/badges-backend/src/types.ts @@ -27,7 +27,7 @@ export const BADGE_STYLES = [ ] as const; /** @public */ -export type BadgeStyle = typeof BADGE_STYLES[number]; +export type BadgeStyle = (typeof BADGE_STYLES)[number]; /** @public */ export interface Badge { diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index 6171688a50..c92817a9e3 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-badges +## 0.2.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.2.39 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## 0.2.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.2.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.2.38 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 8cfb26eb1a..a5a610c041 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.38", + "version": "0.2.40-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -54,7 +54,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index 1b2bec044a..1ae72cadfb 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-bazaar-backend +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + +## 0.2.5 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + +## 0.2.5-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + ## 0.2.3 ### Patch Changes diff --git a/plugins/bazaar-backend/api-report.md b/plugins/bazaar-backend/api-report.md index 1ee889c04a..6281530851 100644 --- a/plugins/bazaar-backend/api-report.md +++ b/plugins/bazaar-backend/api-report.md @@ -3,12 +3,16 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; +// @alpha +export const bazaarPlugin: () => BackendFeature; + // @public (undocumented) export function createRouter(options: RouterOptions): Promise<express.Router>; diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 0636d5573c..e8ea2a19f7 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,20 +1,21 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.2.3", + "version": "0.2.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "backend-plugin" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -23,7 +24,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", @@ -35,10 +36,12 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^" }, "files": [ "dist", - "migrations/**/*.{js,d.ts}" + "migrations/**/*.{js,d.ts}", + "alpha" ] } diff --git a/plugins/bazaar-backend/src/index.ts b/plugins/bazaar-backend/src/index.ts index ca73cb27ba..dd0cdbb750 100644 --- a/plugins/bazaar-backend/src/index.ts +++ b/plugins/bazaar-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export { bazaarPlugin } from './plugin'; diff --git a/plugins/bazaar-backend/src/plugin.ts b/plugins/bazaar-backend/src/plugin.ts new file mode 100644 index 0000000000..62622717c8 --- /dev/null +++ b/plugins/bazaar-backend/src/plugin.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The Bazaar backend plugin. + * + * @alpha + */ +export const bazaarPlugin = createBackendPlugin({ + pluginId: 'bazaar', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + database: coreServices.database, + identity: coreServices.identity, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ database, config, identity, logger, httpRouter }) { + httpRouter.use( + await createRouter({ + database, + config, + identity, + logger: loggerToWinstonLogger(logger), + }), + ); + }, + }); + }, +}); diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index 51f77904c3..49a5570245 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,76 @@ # @backstage/plugin-bazaar +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/cli@0.22.4-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/cli@0.22.2 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.0 + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/cli@0.22.1 + ## 0.2.3 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 2bd2bf709f..ae3703cbbc 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.2.3", + "version": "0.2.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index 5182579a4b..95e6f36836 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -132,7 +132,7 @@ export namespace Models { readonly Plaintext: 'plaintext'; }; export type BaseCommitSummaryMarkupEnum = - typeof BaseCommitSummaryMarkupEnum[keyof typeof BaseCommitSummaryMarkupEnum]; + (typeof BaseCommitSummaryMarkupEnum)[keyof typeof BaseCommitSummaryMarkupEnum]; export interface Branch { default_merge_strategy?: string; // (undocumented) @@ -150,7 +150,7 @@ export namespace Models { readonly FastForward: 'fast_forward'; }; export type BranchMergeStrategiesEnum = - typeof BranchMergeStrategiesEnum[keyof typeof BranchMergeStrategiesEnum]; + (typeof BranchMergeStrategiesEnum)[keyof typeof BranchMergeStrategiesEnum]; export interface Commit extends BaseCommit { // (undocumented) participants?: Array<Participant>; @@ -179,7 +179,7 @@ export namespace Models { }; // (undocumented) export type CommitFileAttributesEnum = - typeof CommitFileAttributesEnum[keyof typeof CommitFileAttributesEnum]; + (typeof CommitFileAttributesEnum)[keyof typeof CommitFileAttributesEnum]; export interface Link { // (undocumented) href?: string; @@ -221,7 +221,7 @@ export namespace Models { }; // (undocumented) export type ParticipantRoleEnum = - typeof ParticipantRoleEnum[keyof typeof ParticipantRoleEnum]; + (typeof ParticipantRoleEnum)[keyof typeof ParticipantRoleEnum]; const // (undocumented) ParticipantStateEnum: { readonly Approved: 'approved'; @@ -230,7 +230,7 @@ export namespace Models { }; // (undocumented) export type ParticipantStateEnum = - typeof ParticipantStateEnum[keyof typeof ParticipantStateEnum]; + (typeof ParticipantStateEnum)[keyof typeof ParticipantStateEnum]; export interface Project extends ModelObject { // (undocumented) created_on?: string; @@ -306,7 +306,7 @@ export namespace Models { readonly NoForks: 'no_forks'; }; export type RepositoryForkPolicyEnum = - typeof RepositoryForkPolicyEnum[keyof typeof RepositoryForkPolicyEnum]; + (typeof RepositoryForkPolicyEnum)[keyof typeof RepositoryForkPolicyEnum]; const // (undocumented) RepositoryScmEnum: { readonly Git: 'git'; @@ -336,7 +336,7 @@ export namespace Models { } // (undocumented) export type RepositoryScmEnum = - typeof RepositoryScmEnum[keyof typeof RepositoryScmEnum]; + (typeof RepositoryScmEnum)[keyof typeof RepositoryScmEnum]; // (undocumented) export interface SearchCodeSearchResult { // (undocumented) diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index d42d00e4f3..da9ebadcca 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -33,7 +33,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@openapitools/openapi-generator-cli": "^2.4.26", - "msw": "^0.49.0", + "msw": "^1.0.0", "ts-morph": "^17.0.0" }, "files": [ diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index 9d25bbb873..d868e2926c 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -107,7 +107,7 @@ export namespace Models { * @public */ export type BaseCommitSummaryMarkupEnum = - typeof BaseCommitSummaryMarkupEnum[keyof typeof BaseCommitSummaryMarkupEnum]; + (typeof BaseCommitSummaryMarkupEnum)[keyof typeof BaseCommitSummaryMarkupEnum]; /** * A branch object, representing a branch in a repository. @@ -146,7 +146,7 @@ export namespace Models { * @public */ export type BranchMergeStrategiesEnum = - typeof BranchMergeStrategiesEnum[keyof typeof BranchMergeStrategiesEnum]; + (typeof BranchMergeStrategiesEnum)[keyof typeof BranchMergeStrategiesEnum]; /** * A repository commit object. @@ -191,7 +191,7 @@ export namespace Models { * @public */ export type CommitFileAttributesEnum = - typeof CommitFileAttributesEnum[keyof typeof CommitFileAttributesEnum]; + (typeof CommitFileAttributesEnum)[keyof typeof CommitFileAttributesEnum]; /** * A link to a resource related to this object. @@ -280,7 +280,7 @@ export namespace Models { * @public */ export type ParticipantRoleEnum = - typeof ParticipantRoleEnum[keyof typeof ParticipantRoleEnum]; + (typeof ParticipantRoleEnum)[keyof typeof ParticipantRoleEnum]; /** * @public @@ -295,7 +295,7 @@ export namespace Models { * @public */ export type ParticipantStateEnum = - typeof ParticipantStateEnum[keyof typeof ParticipantStateEnum]; + (typeof ParticipantStateEnum)[keyof typeof ParticipantStateEnum]; /** * A Bitbucket project. @@ -423,7 +423,7 @@ export namespace Models { * @public */ export type RepositoryForkPolicyEnum = - typeof RepositoryForkPolicyEnum[keyof typeof RepositoryForkPolicyEnum]; + (typeof RepositoryForkPolicyEnum)[keyof typeof RepositoryForkPolicyEnum]; /** * @public @@ -436,7 +436,7 @@ export namespace Models { * @public */ export type RepositoryScmEnum = - typeof RepositoryScmEnum[keyof typeof RepositoryScmEnum]; + (typeof RepositoryScmEnum)[keyof typeof RepositoryScmEnum]; /** * @public diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index e1e8ac0eb1..2c74da08aa 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-bitrise +## 0.1.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.1.42 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.42-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.1.42-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.41 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index ad584a5677..3366b34d9a 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.41", + "version": "0.1.43-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "@types/recharts": "^1.8.15", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.tsx b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.tsx index 0966de3377..3f6ebf93f4 100644 --- a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.tsx +++ b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.tsx @@ -81,7 +81,7 @@ const renderSource = (build: BitriseBuildResult): React.ReactNode => { rel="noreferrer" startIcon={<GitHubIcon />} > - {build.commitHash.substr(0, 6)} + {build.commitHash.slice(0, 6)} </Button> ) : null; }; diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 8234f499c8..085b505782 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,88 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.1.17-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- bf611cf019: Fix missing `dependencies` in `package.json` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-kubernetes-common@0.6.1-next.0 + +## 0.1.15 + +### Patch Changes + +- 671c6af700: Update to import `LocationSpec` from the correct package. +- 8e025f1347: Added support for `externalId` when assuming role in `AwsS3EntityProvider` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## 0.1.15-next.0 + +### Patch Changes + +- 8e025f1347: Added support for `externalId` when assuming role in `AwsS3EntityProvider` +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.1.13 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/alpha-api-report.md b/plugins/catalog-backend-module-aws/alpha-api-report.md new file mode 100644 index 0000000000..8781975085 --- /dev/null +++ b/plugins/catalog-backend-module-aws/alpha-api-report.md @@ -0,0 +1,12 @@ +## 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 { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const awsS3EntityProviderCatalogModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md index 63b85f223b..5086ec1ea7 100644 --- a/plugins/catalog-backend-module-aws/api-report.md +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorParser } from '@backstage/plugin-catalog-backend'; @@ -11,7 +10,7 @@ import { Config } from '@backstage/config'; import { Credentials } from 'aws-sdk'; import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; -import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { Logger } from 'winston'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; @@ -88,7 +87,4 @@ export class AwsS3EntityProvider implements EntityProvider { // (undocumented) refresh(logger: Logger): Promise<void>; } - -// @alpha -export const awsS3EntityProviderCatalogModule: () => BackendFeature; ``` diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index f9ba8e61fb..ea75932dc8 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", "description": "A Backstage catalog backend module that helps integrate towards AWS", - "version": "0.1.13", + "version": "0.1.17-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -41,7 +53,9 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/types": "workspace:^", "aws-sdk": "^2.840.0", "lodash": "^4.17.21", @@ -58,7 +72,6 @@ "yaml": "^2.0.0" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/catalog-backend-module-aws/src/alpha.ts b/plugins/catalog-backend-module-aws/src/alpha.ts new file mode 100644 index 0000000000..3ae30df0cb --- /dev/null +++ b/plugins/catalog-backend-module-aws/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { awsS3EntityProviderCatalogModule } from './service/AwsS3EntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts b/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts index ffb84d1c34..fded78aed4 100644 --- a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts +++ b/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts @@ -27,6 +27,7 @@ export class AwsCredentials { accessKeyId?: string; secretAccessKey?: string; roleArn?: string; + externalId?: string; }, roleSessionName: string, ): Credentials | CredentialsOptions | undefined { @@ -52,6 +53,7 @@ export class AwsCredentials { params: { RoleArn: roleArn, RoleSessionName: roleSessionName, + ExternalId: config.externalId, }, }); } diff --git a/plugins/catalog-backend-module-aws/src/index.ts b/plugins/catalog-backend-module-aws/src/index.ts index 804fd97e91..212308edaa 100644 --- a/plugins/catalog-backend-module-aws/src/index.ts +++ b/plugins/catalog-backend-module-aws/src/index.ts @@ -22,5 +22,4 @@ export * from './processors'; export * from './providers'; -export { awsS3EntityProviderCatalogModule } from './service/AwsS3EntityProviderCatalogModule'; export * from './types'; diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts index 87b7f2c487..ecf3b1a961 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts @@ -20,10 +20,12 @@ import aws from 'aws-sdk'; describe('AwsEKSClusterProcessor', () => { AWSMock.setSDKInstance(aws); + describe('readLocation', () => { const processor = new (AwsEKSClusterProcessor as any)({}); const location = { type: 'aws-eks', target: '957140518395/us-west-2' }; const emit = jest.fn(); + it('generates cluster correctly', async () => { const clusters: aws.EKS.Types.ListClustersResponse = { clusters: ['backstage-test'], @@ -40,11 +42,12 @@ describe('AwsEKSClusterProcessor', () => { }, }, }; - AWSMock.mock('EKS', 'listClusters', clusters); + AWSMock.mock('EKS', 'listClusters', clusters); AWSMock.mock('EKS', 'describeCluster', cluster); await processor.readLocation(location, false, emit); + expect(emit).toHaveBeenCalledWith({ type: 'entity', location, diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts index c34b247f4d..b6418ade03 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { CatalogProcessor, CatalogProcessorEmit, - LocationSpec, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { ANNOTATION_KUBERNETES_API_SERVER, ANNOTATION_KUBERNETES_API_SERVER_CA, ANNOTATION_KUBERNETES_AUTH_PROVIDER, -} from '@backstage/catalog-model'; +} from '@backstage/plugin-kubernetes-common'; import { Credentials, EKS } from 'aws-sdk'; import { AWSCredentialFactory } from '../types'; diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts index 1018e49a41..ecc42e69ea 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts @@ -19,9 +19,9 @@ import { Config } from '@backstage/config'; import { CatalogProcessor, CatalogProcessorEmit, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import AWS, { Credentials, Organizations } from 'aws-sdk'; import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations'; import { Logger } from 'winston'; diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts index 84f2289c94..072c3d8384 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts @@ -20,9 +20,9 @@ import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import limiterFactory from 'p-limit'; /** diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts index b92fcfc022..5e24d4dc13 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts @@ -20,9 +20,9 @@ import { AwsS3Integration, ScmIntegrations } from '@backstage/integration'; import { EntityProvider, EntityProviderConnection, - LocationSpec, locationSpecToLocationEntity, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { AwsCredentials } from '../credentials/AwsCredentials'; import { readAwsS3Configs } from './config'; import { AwsS3Config } from './types'; diff --git a/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.test.ts index c0d36ad3c2..868a9535b4 100644 --- a/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/backend-tasks'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; import { awsS3EntityProviderCatalogModule } from './AwsS3EntityProviderCatalogModule'; import { AwsS3EntityProvider } from '../providers'; diff --git a/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.ts b/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.ts index cbc137ab40..5d4170ec1c 100644 --- a/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.ts @@ -19,7 +19,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { AwsS3EntityProvider } from '../providers'; /** diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index bb61f34884..3dbd990f57 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,138 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.14-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.13 + +### Patch Changes + +- 2890f47517: This will add the ability to use Azure DevOps with multi project with a single value which is a new feature as previously this had to be done manually for each project that you wanted to add. + + Right now you would have to fill in multiple values in the config to use multiple projects: + + ``` + yourFirstProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: 'firstProject' # this will match the firstProject project + repository: '*' # this will match all repos + path: /catalog-info.yaml + yourSecondProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: 'secondProject' # this will match the secondProject project + repository: '*' # this will match all repos + path: /catalog-info.yaml + ``` + + With this change you can actually have all projects available where your PAT determines which you have access to, so that includes multiple projects: + + ``` + yourFirstProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: '*' # this will match all projects where your PAT has access to + repository: '*' # this will match all repos + path: /catalog-info.yaml + ``` + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.13-next.1 + +### Patch Changes + +- 2890f47517: This will add the ability to use Azure DevOps with multi project with a single value which is a new feature as previously this had to be done manually for each project that you wanted to add. + + Right now you would have to fill in multiple values in the config to use multiple projects: + + ``` + yourFirstProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: 'firstProject' # this will match the firstProject project + repository: '*' # this will match all repos + path: /catalog-info.yaml + yourSecondProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: 'secondProject' # this will match the secondProject project + repository: '*' # this will match all repos + path: /catalog-info.yaml + ``` + + With this change you can actually have all projects available where your PAT determines which you have access to, so that includes multiple projects: + + ``` + yourFirstProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: '*' # this will match all projects where your PAT has access to + repository: '*' # this will match all repos + path: /catalog-info.yaml + ``` + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.1.11 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/alpha-api-report.md b/plugins/catalog-backend-module-azure/alpha-api-report.md new file mode 100644 index 0000000000..486aac7775 --- /dev/null +++ b/plugins/catalog-backend-module-azure/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-azure" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const azureDevOpsEntityProviderCatalogModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-azure/api-report.md b/plugins/catalog-backend-module-azure/api-report.md index 5ffae14b6b..8fb7edeb53 100644 --- a/plugins/catalog-backend-module-azure/api-report.md +++ b/plugins/catalog-backend-module-azure/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; @@ -56,7 +55,4 @@ export class AzureDevOpsEntityProvider implements EntityProvider { // (undocumented) refresh(logger: Logger): Promise<void>; } - -// @alpha -export const azureDevOpsEntityProviderCatalogModule: () => BackendFeature; ``` diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 292edb0e80..1594e88955 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", "description": "A Backstage catalog backend module that helps integrate towards Azure", - "version": "0.1.11", + "version": "0.1.14-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -53,10 +65,9 @@ "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "luxon": "^3.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/catalog-backend-module-azure/src/alpha.ts b/plugins/catalog-backend-module-azure/src/alpha.ts new file mode 100644 index 0000000000..cc3c4a83af --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { azureDevOpsEntityProviderCatalogModule } from './service/AzureDevOpsEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-azure/src/index.ts b/plugins/catalog-backend-module-azure/src/index.ts index aa3a94fda2..a71c40ee6c 100644 --- a/plugins/catalog-backend-module-azure/src/index.ts +++ b/plugins/catalog-backend-module-azure/src/index.ts @@ -22,4 +22,3 @@ export { AzureDevOpsDiscoveryProcessor } from './processors'; export { AzureDevOpsEntityProvider } from './providers'; -export { azureDevOpsEntityProviderCatalogModule } from './service/AzureDevOpsEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts index 16f5067197..f26208353a 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -29,11 +29,11 @@ describe('azure', () => { server.use( rest.post( - `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + `https://almsearch.dev.azure.com/shopify/_apis/search/codesearchresults`, (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ - searchText: 'path:/catalog-info.yaml repo:*', + searchText: 'path:/catalog-info.yaml repo:* proj:engineering', $skip: 0, $top: 1000, }); @@ -64,6 +64,9 @@ describe('azure', () => { repository: { name: 'backstage', }, + project: { + name: 'backstage', + }, }, { fileName: 'catalog-info.yaml', @@ -71,17 +74,20 @@ describe('azure', () => { repository: { name: 'ios-app', }, + project: { + name: 'backstage', + }, }, ], }; server.use( rest.post( - `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + `https://almsearch.dev.azure.com/shopify/_apis/search/codesearchresults`, (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ - searchText: 'path:/catalog-info.yaml repo:*', + searchText: 'path:/catalog-info.yaml repo:* proj:engineering', $skip: 0, $top: 1000, }); @@ -108,6 +114,9 @@ describe('azure', () => { { fileName: 'catalog-info.yaml', path: '/catalog-info.yaml', + project: { + name: '*', + }, repository: { name: 'backstage', }, @@ -117,11 +126,12 @@ describe('azure', () => { server.use( rest.post( - `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + `https://almsearch.dev.azure.com/shopify/_apis/search/codesearchresults`, (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ - searchText: 'path:/catalog-info.yaml repo:backstage', + searchText: + 'path:/catalog-info.yaml repo:backstage proj:engineering', $skip: 0, $top: 1000, }); @@ -151,17 +161,20 @@ describe('azure', () => { repository: { name: 'backstage', }, + project: { + name: '*', + }, }, ], }; server.use( rest.post( - `https://azuredevops.mycompany.com/shopify/engineering/_apis/search/codesearchresults`, + `https://azuredevops.mycompany.com/shopify/_apis/search/codesearchresults`, (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ - searchText: 'path:/catalog-info.yaml repo:*', + searchText: 'path:/catalog-info.yaml repo:* proj:engineering', $skip: 0, $top: 1000, }); @@ -190,16 +203,20 @@ describe('azure', () => { repository: { name: 'backstage', }, + project: { + name: 'engineering', + }, })); }; server.use( rest.post( - `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + `https://almsearch.dev.azure.com/shopify/_apis/search/codesearchresults`, (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toMatchObject({ - searchText: 'path:/catalog-info.yaml repo:backstage', + searchText: + 'path:/catalog-info.yaml repo:backstage proj:engineering', $top: 1000, }); diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.ts b/plugins/catalog-backend-module-azure/src/lib/azure.ts index 316c9b0b8c..0c96d133d3 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.ts @@ -31,6 +31,9 @@ export interface CodeSearchResultItem { repository: { name: string; }; + project: { + name: string; + }; } const isCloud = (host: string) => host === 'dev.azure.com'; @@ -47,7 +50,7 @@ export async function codeSearch( const searchBaseUrl = isCloud(azureConfig.host) ? 'https://almsearch.dev.azure.com' : `https://${azureConfig.host}`; - const searchUrl = `${searchBaseUrl}/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`; + const searchUrl = `${searchBaseUrl}/${org}/_apis/search/codesearchresults?api-version=6.0-preview.1`; let items: CodeSearchResultItem[] = []; let hasMorePages = true; @@ -59,7 +62,7 @@ export async function codeSearch( }), method: 'POST', body: JSON.stringify({ - searchText: `path:${path} repo:${repo || '*'}`, + searchText: `path:${path} repo:${repo || '*'} proj:${project || '*'}`, $skip: items.length, $top: PAGE_SIZE, }), diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts index 9000b177fa..38df737179 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -135,6 +135,9 @@ describe('AzureDevOpsDiscoveryProcessor', () => { { fileName: 'catalog-info.yaml', path: '/catalog-info.yaml', + project: { + name: '*', + }, repository: { name: 'backstage', }, @@ -142,6 +145,9 @@ describe('AzureDevOpsDiscoveryProcessor', () => { { fileName: 'catalog-info.yaml', path: '/src/catalog-info.yaml', + project: { + name: '*', + }, repository: { name: 'ios-app', }, @@ -191,6 +197,9 @@ describe('AzureDevOpsDiscoveryProcessor', () => { repository: { name: 'backstage', }, + project: { + name: '*', + }, }, ]); const emitter = jest.fn(); @@ -229,6 +238,9 @@ describe('AzureDevOpsDiscoveryProcessor', () => { repository: { name: 'backstage', }, + project: { + name: '*', + }, }, ]); const emitter = jest.fn(); diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts index ad63fef810..8893d5d42d 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts @@ -43,7 +43,7 @@ import { codeSearch } from '../lib'; * target: https://dev.azure.com/org/project/_git/repo * * @public - **/ + */ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; @@ -123,7 +123,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { /** * parseUrl extracts segments from the Azure DevOps URL. - **/ + */ export function parseUrl(urlString: string): { baseUrl: string; org: string; @@ -132,7 +132,7 @@ export function parseUrl(urlString: string): { catalogPath: string; } { const url = new URL(urlString); - const path = url.pathname.substr(1).split('/'); + const path = url.pathname.slice(1).split('/'); const catalogPath = url.searchParams.get('path') || '/catalog-info.yaml'; diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.test.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.test.ts index f7a63f259e..dff1164899 100644 --- a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.test.ts +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.test.ts @@ -164,6 +164,9 @@ describe('AzureDevOpsEntityProvider', () => { repository: { name: 'myrepo', }, + project: { + name: 'myproject', + }, }, ], 'https://dev.azure.com/myorganization/myproject', @@ -189,6 +192,9 @@ describe('AzureDevOpsEntityProvider', () => { repository: { name: 'myrepo', }, + project: { + name: 'myproject', + }, }, { fileName: 'catalog-info.yaml', @@ -196,6 +202,9 @@ describe('AzureDevOpsEntityProvider', () => { repository: { name: 'myotherrepo', }, + project: { + name: 'myproject', + }, }, ], 'https://dev.azure.com/myorganization/myproject', @@ -277,6 +286,9 @@ describe('AzureDevOpsEntityProvider', () => { repository: { name: 'myrepo', }, + project: { + name: 'myproject', + }, }, { fileName: 'catalog-info.yaml', @@ -284,6 +296,9 @@ describe('AzureDevOpsEntityProvider', () => { repository: { name: 'myotherrepo', }, + project: { + name: 'myproject', + }, }, ], 'https://dev.azure.com/myorganization/myproject', diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts index 98b951bd46..788b9c2f05 100644 --- a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts @@ -166,15 +166,17 @@ export class AzureDevOpsEntityProvider implements EntityProvider { } private createLocationSpec(file: CodeSearchResultItem): LocationSpec { + const target = this.createObjectUrl(file); + return { type: 'url', - target: this.createObjectUrl(file), + target: target, presence: 'required', }; } private createObjectUrl(file: CodeSearchResultItem): string { - const baseUrl = `https://${this.config.host}/${this.config.organization}/${this.config.project}`; + const baseUrl = `https://${this.config.host}/${this.config.organization}/${file.project.name}`; return encodeURI( `${baseUrl}/_git/${file.repository.name}?path=${file.path}`, ); diff --git a/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.test.ts index a11e6b14f2..a8e526e871 100644 --- a/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/backend-tasks'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; import { azureDevOpsEntityProviderCatalogModule } from './AzureDevOpsEntityProviderCatalogModule'; import { AzureDevOpsEntityProvider } from '../providers'; diff --git a/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.ts index bd59cec29b..3fc1ddf3a4 100644 --- a/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.ts @@ -19,7 +19,7 @@ import { createBackendModule, coreServices, } from '@backstage/backend-plugin-api'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { AzureDevOpsEntityProvider } from '../providers'; /** diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index da1cf2f177..c506ddc588 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,93 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.1.10-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + - @backstage/plugin-catalog-common@1.0.11 + +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + - @backstage/plugin-catalog-common@1.0.11-next.1 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.1.7 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/alpha-api-report.md b/plugins/catalog-backend-module-bitbucket-cloud/alpha-api-report.md new file mode 100644 index 0000000000..77b4b77410 --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket-cloud/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-bitbucket-cloud" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha (undocumented) +export const bitbucketCloudEntityProviderCatalogModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md index 7ad6b3b3e0..456ea62dd3 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-backend'; @@ -46,7 +45,4 @@ export class BitbucketCloudEntityProvider // (undocumented) supportsEventTopics(): string[]; } - -// @alpha (undocumented) -export const bitbucketCloudEntityProviderCatalogModule: () => BackendFeature; ``` diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 142fb4617d..470ab9b813 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", - "version": "0.1.7", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -53,10 +65,9 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "luxon": "^3.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/alpha.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/alpha.ts new file mode 100644 index 0000000000..e949dcb894 --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { bitbucketCloudEntityProviderCatalogModule } from './service/BitbucketCloudEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/index.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/index.ts index c1f04b8877..1c15ad4e8f 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/index.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/index.ts @@ -21,4 +21,3 @@ */ export { BitbucketCloudEntityProvider } from './BitbucketCloudEntityProvider'; -export { bitbucketCloudEntityProviderCatalogModule } from './service/BitbucketCloudEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.test.ts index d44281600c..630c18b134 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.test.ts @@ -19,12 +19,9 @@ import { PluginTaskScheduler, TaskScheduleDefinition, } from '@backstage/backend-tasks'; -import { - startTestBackend, - mockConfigFactory, -} from '@backstage/backend-test-utils'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { startTestBackend, mockServices } from '@backstage/backend-test-utils'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { Duration } from 'luxon'; import { bitbucketCloudEntityProviderCatalogModule } from './BitbucketCloudEntityProviderCatalogModule'; import { BitbucketCloudEntityProvider } from '../BitbucketCloudEntityProvider'; @@ -59,7 +56,7 @@ describe('bitbucketCloudEntityProviderCatalogModule', () => { [eventsExtensionPoint, eventsExtensionPointImpl], ], services: [ - mockConfigFactory({ + mockServices.config.factory({ data: { catalog: { providers: { diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.ts index d17656a473..a95d99ceef 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.ts @@ -22,8 +22,8 @@ import { import { catalogProcessingExtensionPoint, catalogServiceRef, -} from '@backstage/plugin-catalog-node'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +} from '@backstage/plugin-catalog-node/alpha'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { BitbucketCloudEntityProvider } from '../BitbucketCloudEntityProvider'; /** diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index efcb84d975..938f731df3 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,78 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.8-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.1.5 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/alpha-api-report.md b/plugins/catalog-backend-module-bitbucket-server/alpha-api-report.md new file mode 100644 index 0000000000..3177828f51 --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket-server/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-bitbucket-server" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha (undocumented) +export const bitbucketServerEntityProviderCatalogModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-bitbucket-server/api-report.md b/plugins/catalog-backend-module-bitbucket-server/api-report.md index 213f5d2573..6c840883af 100644 --- a/plugins/catalog-backend-module-bitbucket-server/api-report.md +++ b/plugins/catalog-backend-module-bitbucket-server/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; @@ -68,9 +67,6 @@ export class BitbucketServerEntityProvider implements EntityProvider { refresh(logger: Logger): Promise<void>; } -// @alpha (undocumented) -export const bitbucketServerEntityProviderCatalogModule: () => BackendFeature; - // @public (undocumented) export type BitbucketServerListOptions = { [key: string]: number | undefined; diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 4c19df0582..d96e7c91f9 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,14 +1,26 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.1.5", + "version": "0.1.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" @@ -24,7 +36,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -50,10 +62,9 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "luxon": "^3.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts b/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts new file mode 100644 index 0000000000..d6227c29f1 --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { bitbucketServerEntityProviderCatalogModule } from './service/BitbucketServerEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/index.ts b/plugins/catalog-backend-module-bitbucket-server/src/index.ts index f139078cfb..34ac3a9966 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/index.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/index.ts @@ -29,4 +29,3 @@ export type { } from './lib'; export { BitbucketServerEntityProvider } from './providers'; export type { BitbucketServerLocationParser } from './providers'; -export { bitbucketServerEntityProviderCatalogModule } from './service/BitbucketServerEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.test.ts index 474431dc7a..bdd7209364 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.test.ts @@ -22,7 +22,7 @@ import { TaskScheduleDefinition, } from '@backstage/backend-tasks'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { bitbucketServerEntityProviderCatalogModule } from './BitbucketServerEntityProviderCatalogModule'; import { Duration } from 'luxon'; import { BitbucketServerEntityProvider } from '../providers'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.ts index 46712d29dd..deab47c9e9 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.ts @@ -19,7 +19,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { BitbucketServerEntityProvider } from '../providers'; /** diff --git a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md index 12f7317c25..d082563115 100644 --- a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-catalog-backend-module-bitbucket +## 0.2.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## 0.2.9 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## 0.2.9-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + ## 0.2.7 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket/package.json b/plugins/catalog-backend-module-bitbucket/package.json index 43bce0f00e..4f10b4c567 100644 --- a/plugins/catalog-backend-module-bitbucket/package.json +++ b/plugins/catalog-backend-module-bitbucket/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket", - "version": "0.2.7", + "version": "0.2.10-next.0", "deprecated": true, "main": "src/index.ts", "types": "src/index.ts", @@ -49,7 +49,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/catalog-backend-module-bitbucket/src/BitbucketDiscoveryProcessor.ts b/plugins/catalog-backend-module-bitbucket/src/BitbucketDiscoveryProcessor.ts index 6924765483..78aa3eacfd 100644 --- a/plugins/catalog-backend-module-bitbucket/src/BitbucketDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-bitbucket/src/BitbucketDiscoveryProcessor.ts @@ -342,7 +342,7 @@ function parseUrl(urlString: string): { const url = new URL(urlString); const indexOfProjectSegment = url.pathname.toLowerCase().indexOf('/projects/') + 1; - const path = url.pathname.substr(indexOfProjectSegment).split('/'); + const path = url.pathname.slice(indexOfProjectSegment).split('/'); // /projects/backstage/repos/techdocs-*/catalog-info.yaml if (path.length > 3 && path[1].length && path[3].length) { @@ -373,7 +373,7 @@ function parseBitbucketCloudUrl(urlString: string): { searchEnabled: boolean; } { const url = new URL(urlString); - const pathMap = readPathParameters(url.pathname.substr(1).split('/')); + const pathMap = readPathParameters(url.pathname.slice(1).split('/')); const query = url.searchParams; if (!pathMap.has('workspaces')) { diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index 0ee1a11e24..80a5989141 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,78 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.11-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/alpha-api-report.md b/plugins/catalog-backend-module-gerrit/alpha-api-report.md new file mode 100644 index 0000000000..03f5d099b9 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-gerrit" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha (undocumented) +export const gerritEntityProviderCatalogModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-gerrit/api-report.md b/plugins/catalog-backend-module-gerrit/api-report.md index 870937f7ec..1dee786da1 100644 --- a/plugins/catalog-backend-module-gerrit/api-report.md +++ b/plugins/catalog-backend-module-gerrit/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; @@ -30,8 +29,5 @@ export class GerritEntityProvider implements EntityProvider { refresh(logger: Logger): Promise<void>; } -// @alpha (undocumented) -export const gerritEntityProviderCatalogModule: () => BackendFeature; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 3638237a9e..b5482affb1 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,14 +1,26 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.1.8", + "version": "0.1.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" @@ -21,7 +33,7 @@ }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -48,7 +60,7 @@ "@backstage/cli": "workspace:^", "@types/fs-extra": "^9.0.1", "luxon": "^3.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/catalog-backend-module-gerrit/src/alpha.ts b/plugins/catalog-backend-module-gerrit/src/alpha.ts new file mode 100644 index 0000000000..8b03d9312a --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { gerritEntityProviderCatalogModule } from './service/GerritEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-gerrit/src/index.ts b/plugins/catalog-backend-module-gerrit/src/index.ts index 38ede69926..133772d4b1 100644 --- a/plugins/catalog-backend-module-gerrit/src/index.ts +++ b/plugins/catalog-backend-module-gerrit/src/index.ts @@ -15,4 +15,3 @@ */ export { GerritEntityProvider } from './providers/GerritEntityProvider'; -export { gerritEntityProviderCatalogModule } from './service/GerritEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.test.ts index 9d61cabdb9..51c1749c66 100644 --- a/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/backend-tasks'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; import { gerritEntityProviderCatalogModule } from './GerritEntityProviderCatalogModule'; import { GerritEntityProvider } from '../providers/GerritEntityProvider'; diff --git a/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.ts index 8c61038c8f..782c724094 100644 --- a/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.ts @@ -19,7 +19,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { GerritEntityProvider } from '../providers/GerritEntityProvider'; /** diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index a942ba0f20..e4b14ca727 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,102 @@ # @backstage/plugin-catalog-backend-module-github +## 0.2.6-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.2.5 + +### Patch Changes + +- 66158754b4: Add support for filtering out forks +- 80afd5ff8b: Use github team slug and login annotations rather than entity name to infer location annotations +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + +## 0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + +## 0.2.5-next.1 + +### Patch Changes + +- 66158754b4: Add support for filtering out forks +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.2.3 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/alpha-api-report.md b/plugins/catalog-backend-module-github/alpha-api-report.md new file mode 100644 index 0000000000..eb13ec4558 --- /dev/null +++ b/plugins/catalog-backend-module-github/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-github" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const githubEntityProviderCatalogModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-github/api-report.md b/plugins/catalog-backend-module-github/api-report.md index a66ec712c7..e2b73d267b 100644 --- a/plugins/catalog-backend-module-github/api-report.md +++ b/plugins/catalog-backend-module-github/api-report.md @@ -4,7 +4,6 @@ ```ts import { AnalyzeOptions } from '@backstage/plugin-catalog-backend'; -import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; @@ -100,9 +99,6 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { supportsEventTopics(): string[]; } -// @alpha -export const githubEntityProviderCatalogModule: () => BackendFeature; - // @public (undocumented) export class GithubLocationAnalyzer implements ScmLocationAnalyzer { constructor(options: GithubLocationAnalyzerOptions); diff --git a/plugins/catalog-backend-module-github/config.d.ts b/plugins/catalog-backend-module-github/config.d.ts index 691caedc46..37852223c1 100644 --- a/plugins/catalog-backend-module-github/config.d.ts +++ b/plugins/catalog-backend-module-github/config.d.ts @@ -86,6 +86,10 @@ export interface Config { /** * (Optional) GitHub topic-based filters. */ + allowForks?: boolean; + /** + * (Optional) Allow Forks to be evaluated. + */ topic?: { /** * (Optional) An array of strings used to filter in results based on their associated GitHub topics. @@ -143,6 +147,10 @@ export interface Config { /** * (Optional) GitHub topic-based filters. */ + allowForks?: boolean; + /** + * (Optional) Allow Forks to be evaluated. + */ topic?: { /** * (Optional) An array of strings used to filter in results based on their associated GitHub topics. diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index 57b72cebe9..5a528a8e05 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-catalog-backend-module-github", "description": "A Backstage catalog backend module that helps integrate towards GitHub", - "version": "0.2.3", + "version": "0.2.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" @@ -24,7 +36,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -60,11 +72,10 @@ "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "luxon": "^3.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", - "alpha", "config.d.ts" ], "configSchema": "config.d.ts" diff --git a/plugins/catalog-backend-module-github/src/alpha.ts b/plugins/catalog-backend-module-github/src/alpha.ts new file mode 100644 index 0000000000..1b83eead82 --- /dev/null +++ b/plugins/catalog-backend-module-github/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { githubEntityProviderCatalogModule } from './service/GithubEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-github/src/index.ts b/plugins/catalog-backend-module-github/src/index.ts index b005780758..17c769419f 100644 --- a/plugins/catalog-backend-module-github/src/index.ts +++ b/plugins/catalog-backend-module-github/src/index.ts @@ -28,7 +28,6 @@ export { GithubOrgReaderProcessor } from './processors/GithubOrgReaderProcessor' export { GithubEntityProvider } from './providers/GithubEntityProvider'; export { GithubOrgEntityProvider } from './providers/GithubOrgEntityProvider'; export type { GithubOrgEntityProviderOptions } from './providers/GithubOrgEntityProvider'; -export { githubEntityProviderCatalogModule } from './service/GithubEntityProviderCatalogModule'; export { type GithubMultiOrgConfig, type GithubTeam, diff --git a/plugins/catalog-backend-module-github/src/lib/annotation.ts b/plugins/catalog-backend-module-github/src/lib/annotation.ts new file mode 100644 index 0000000000..3b2f7b87a8 --- /dev/null +++ b/plugins/catalog-backend-module-github/src/lib/annotation.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The value of this annotation is the so-called login that identifies a user on +[GitHub](https://github.com) (either the public one, or a private GitHub +Enterprise installation) that is related to this entity. It is on the format +`<username>`, and is the same as can be seen in the URL location bar of the +browser when viewing that user. + * + * @public + */ +export const ANNOTATION_GITHUB_USER_LOGIN = 'github.com/user-login'; + +/** + * The value of this annotation is the so-called slug that identifies a team on +[GitHub](https://github.com) (either the public one, or a private GitHub +Enterprise installation) that is related to this entity. It is on the format +`<organization>/<team>`, and is the same as can be seen in the URL location bar +of the browser when viewing that team. + * + * @public + */ +export const ANNOTATION_GITHUB_TEAM_SLUG = 'github.com/team-slug'; diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts index efd3eedd5d..7669fd5b47 100644 --- a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts @@ -16,6 +16,10 @@ import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import { graphql } from '@octokit/graphql'; +import { + ANNOTATION_GITHUB_TEAM_SLUG, + ANNOTATION_GITHUB_USER_LOGIN, +} from './annotation'; import { GithubTeam, GithubUser } from './github'; /** @@ -63,7 +67,7 @@ export const defaultUserTransformer: UserTransformer = async ( metadata: { name: item.login, annotations: { - 'github.com/user-login': item.login, + [ANNOTATION_GITHUB_USER_LOGIN]: item.login, }, }, spec: { @@ -87,7 +91,7 @@ export const defaultUserTransformer: UserTransformer = async ( export const defaultOrganizationTeamTransformer: TeamTransformer = async team => { const annotations: { [annotationName: string]: string } = { - 'github.com/team-slug': team.combinedSlug, + [ANNOTATION_GITHUB_TEAM_SLUG]: team.combinedSlug, }; if (team.editTeamUrl) { diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index 387dc8a2ee..624ea5c3c9 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -488,6 +488,7 @@ describe('github', () => { name: 'backstage', url: 'https://github.com/backstage/backstage', isArchived: false, + isFork: false, repositoryTopics: { nodes: [{ topic: { name: 'blah' } }], }, @@ -500,6 +501,7 @@ describe('github', () => { name: 'demo', url: 'https://github.com/backstage/demo', isArchived: true, + isFork: true, repositoryTopics: { nodes: [] }, defaultBranchRef: { name: 'main', @@ -524,6 +526,7 @@ describe('github', () => { name: 'backstage', url: 'https://github.com/backstage/backstage', isArchived: false, + isFork: false, repositoryTopics: { nodes: [{ topic: { name: 'blah' } }], }, @@ -536,6 +539,7 @@ describe('github', () => { name: 'demo', url: 'https://github.com/backstage/demo', isArchived: true, + isFork: true, repositoryTopics: { nodes: [] }, defaultBranchRef: { name: 'main', diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index f719e3d506..ae6e14fc23 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -89,6 +89,7 @@ export type RepositoryResponse = { name: string; url: string; isArchived: boolean; + isFork: boolean; repositoryTopics: RepositoryTopics; defaultBranchRef: { name: string; @@ -435,6 +436,7 @@ export async function getOrganizationRepositories( } url isArchived + isFork repositoryTopics(first: 100) { nodes { ... on RepositoryTopic { diff --git a/plugins/catalog-backend-module-github/src/lib/util.test.ts b/plugins/catalog-backend-module-github/src/lib/util.test.ts index 7aca6839ea..0de1d24bbf 100644 --- a/plugins/catalog-backend-module-github/src/lib/util.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/util.test.ts @@ -15,7 +15,11 @@ */ import { GithubTopicFilters } from '../providers/GithubEntityProviderConfig'; -import { parseGithubOrgUrl, satisfiesTopicFilter } from './util'; +import { + parseGithubOrgUrl, + satisfiesTopicFilter, + satisfiesForkFilter, +} from './util'; describe('parseGithubOrgUrl', () => { it('only supports clean org urls, and decodes them', () => { @@ -87,3 +91,21 @@ describe('satisfiesTopicFilter', () => { ).toEqual(false); }); }); + +describe('satisfiesForkFilter', () => { + it('handles cases where forks are not allowed and a fork is evaluated', () => { + expect(satisfiesForkFilter(false, true)).toEqual(false); + }); + + it('handles cases where forks are not allowed and a fork is not evaluated', () => { + expect(satisfiesForkFilter(false, false)).toEqual(true); + }); + + it('handles cases where forks are allowed and a fork is evaluated', () => { + expect(satisfiesForkFilter(true, true)).toEqual(true); + }); + + it('handles cases where forks are allowed and a fork is not evaluated', () => { + expect(satisfiesForkFilter(true, false)).toEqual(true); + }); +}); diff --git a/plugins/catalog-backend-module-github/src/lib/util.ts b/plugins/catalog-backend-module-github/src/lib/util.ts index ece463bb40..54ee575a6c 100644 --- a/plugins/catalog-backend-module-github/src/lib/util.ts +++ b/plugins/catalog-backend-module-github/src/lib/util.ts @@ -17,7 +17,7 @@ import { GithubTopicFilters } from '../providers/GithubEntityProviderConfig'; export function parseGithubOrgUrl(urlString: string): { org: string } { - const path = new URL(urlString).pathname.substr(1).split('/'); + const path = new URL(urlString).pathname.slice(1).split('/'); // /backstage if (path.length === 1 && path[0].length) { @@ -77,3 +77,25 @@ export function satisfiesTopicFilter( // not configured at all. return true; } + +export function satisfiesForkFilter( + allowForks: boolean | true, + isFork: boolean | false, +): Boolean { + // we don't want to include forks if forks are not allowed + if (!allowForks && isFork) return false; + + // if forks are allowed, allow all repos through + return true; +} + +// Given the github organisation team slug, returns a tuple containing [organisation, team] +export function splitTeamSlug(slug: string): [string, string] { + const parts = slug.split('/'); + if (parts.length !== 2) { + throw new Error( + `Github team slug '${slug}' was not in the expected format <organisation>/<team>`, + ); + } + return [parts[0], parts[1]]; +} diff --git a/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.test.ts index b69728cc62..f30007c677 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.test.ts @@ -150,6 +150,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/backstage', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'master', }, @@ -160,6 +161,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/demo', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -202,6 +204,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/tech-docs', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -236,6 +239,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/tech-docs', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: null, catalogInfoFile: null, }, @@ -260,6 +264,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/backstage', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'master', }, @@ -295,6 +300,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/backstage', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -305,6 +311,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/techdocs-cli', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -315,6 +322,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/techdocs-container', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -325,6 +333,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/techdocs-durp', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: null, catalogInfoFile: null, }, @@ -365,6 +374,7 @@ describe('GithubDiscoveryProcessor', () => { name: 'abstest', url: 'https://github.com/backstage/abctest', isArchived: false, + isFork: false, repositoryTopics: { nodes: [] }, defaultBranchRef: { name: 'main', @@ -375,6 +385,7 @@ describe('GithubDiscoveryProcessor', () => { name: 'test', url: 'https://github.com/backstage/test', isArchived: false, + isFork: false, repositoryTopics: { nodes: [] }, defaultBranchRef: { name: 'main', @@ -386,6 +397,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/test', repositoryTopics: { nodes: [] }, isArchived: true, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -396,6 +408,7 @@ describe('GithubDiscoveryProcessor', () => { url: 'https://github.com/backstage/testxyz', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, diff --git a/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.ts index 593f5f72f1..c6733df13b 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.ts @@ -46,7 +46,7 @@ import { getOrganizationRepositories } from '../lib'; * target: https://github.com/backstage/*\/blob/main/catalog-info.yaml * * @public - **/ + */ export class GithubDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; @@ -176,7 +176,7 @@ export function parseUrl(urlString: string): { host: string; } { const url = new URL(urlString); - const path = url.pathname.substr(1).split('/'); + const path = url.pathname.slice(1).split('/'); // /backstage/techdocs-*/blob/master/catalog-info.yaml // can also be diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts index 9916825cdc..2be5df09fe 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts @@ -172,6 +172,7 @@ describe('GithubEntityProvider', () => { url: 'https://github.com/test-org/test-repo', repositoryTopics: { nodes: [] }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -274,6 +275,7 @@ describe('GithubEntityProvider', () => { ], }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -353,6 +355,7 @@ describe('GithubEntityProvider', () => { ], }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -445,6 +448,7 @@ describe('GithubEntityProvider', () => { nodes: [], }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -457,6 +461,7 @@ describe('GithubEntityProvider', () => { nodes: [], }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -557,6 +562,7 @@ describe('GithubEntityProvider', () => { ], }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -580,6 +586,7 @@ describe('GithubEntityProvider', () => { ], }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, @@ -600,6 +607,7 @@ describe('GithubEntityProvider', () => { ], }, isArchived: false, + isFork: false, defaultBranchRef: { name: 'main', }, diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts index 1344e2f5d1..3c6fd43bd0 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts @@ -40,7 +40,7 @@ import { GithubEntityProviderConfig, } from './GithubEntityProviderConfig'; import { getOrganizationRepositories } from '../lib/github'; -import { satisfiesTopicFilter } from '../lib/util'; +import { satisfiesTopicFilter, satisfiesForkFilter } from '../lib/util'; import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; import { PushEvent, Commit } from '@octokit/webhooks-types'; @@ -52,6 +52,7 @@ type Repository = { name: string; url: string; isArchived: boolean; + isFork: boolean; repositoryTopics: string[]; defaultBranchRef?: string; isCatalogInfoFilePresent: boolean; @@ -216,6 +217,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { defaultBranchRef: r.defaultBranchRef?.name, repositoryTopics: r.repositoryTopics.nodes.map(t => t.topic.name), isArchived: r.isArchived, + isFork: r.isFork, isCatalogInfoFilePresent: r.catalogInfoFile?.__typename === 'Blob' && r.catalogInfoFile.text !== '', @@ -234,6 +236,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { private matchesFilters(repositories: Repository[]) { const repositoryFilter = this.config.filters?.repository; const topicFilters = this.config.filters?.topic; + const allowForks = this.config.filters?.allowForks ?? true; const matchingRepositories = repositories.filter(r => { const repoTopics: string[] = r.repositoryTopics; @@ -241,6 +244,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { !r.isArchived && (!repositoryFilter || repositoryFilter.test(r.name)) && satisfiesTopicFilter(repoTopics, topicFilters) && + satisfiesForkFilter(allowForks, r.isFork) && r.defaultBranchRef ); }); @@ -302,6 +306,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { defaultBranchRef: event.repository.default_branch, repositoryTopics: event.repository.topics, isArchived: event.repository.archived, + isFork: event.repository.fork, // we can consider this file present because // only the catalog file will be recovered from the commits isCatalogInfoFilePresent: true, @@ -423,7 +428,7 @@ export function parseUrl(urlString: string): { host: string; } { const url = new URL(urlString); - const path = url.pathname.substr(1).split('/'); + const path = url.pathname.slice(1).split('/'); // /backstage/techdocs-*/blob/master/catalog-info.yaml // can also be diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts index 0d9b5ce65b..a597e9dbe0 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts @@ -78,6 +78,12 @@ describe('readProviderConfigs', () => { }, }, }, + providerWithForkFilter: { + organization: 'test-org6', + filters: { + allowForks: false, + }, + }, providerWithHost: { organization: 'test-org1', host: 'ghe.internal.com', @@ -97,7 +103,7 @@ describe('readProviderConfigs', () => { }); const providerConfigs = readProviderConfigs(config); - expect(providerConfigs).toHaveLength(7); + expect(providerConfigs).toHaveLength(8); expect(providerConfigs[0]).toEqual({ id: 'providerOrganizationOnly', organization: 'test-org1', @@ -106,6 +112,7 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: undefined, + allowForks: true, topic: { include: undefined, exclude: undefined, @@ -122,6 +129,7 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: undefined, + allowForks: true, topic: { include: undefined, exclude: undefined, @@ -138,6 +146,7 @@ describe('readProviderConfigs', () => { filters: { repository: /^repository.*filter$/, // repo branch: undefined, // branch + allowForks: true, topic: { include: undefined, exclude: undefined, @@ -154,6 +163,7 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: 'branch-name', + allowForks: true, topic: { include: undefined, exclude: undefined, @@ -170,6 +180,7 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: undefined, + allowForks: true, topic: { include: ['backstage-include'], exclude: ['backstage-exclude'], @@ -179,6 +190,23 @@ describe('readProviderConfigs', () => { validateLocationsExist: false, }); expect(providerConfigs[5]).toEqual({ + id: 'providerWithForkFilter', + organization: 'test-org6', + catalogPath: '/catalog-info.yaml', + host: 'github.com', + filters: { + repository: undefined, + branch: undefined, + allowForks: false, + topic: { + include: undefined, + exclude: undefined, + }, + }, + schedule: undefined, + validateLocationsExist: false, + }); + expect(providerConfigs[6]).toEqual({ id: 'providerWithHost', organization: 'test-org1', catalogPath: '/catalog-info.yaml', @@ -186,6 +214,7 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: undefined, + allowForks: true, topic: { include: undefined, exclude: undefined, @@ -194,7 +223,7 @@ describe('readProviderConfigs', () => { validateLocationsExist: false, schedule: undefined, }); - expect(providerConfigs[6]).toEqual({ + expect(providerConfigs[7]).toEqual({ id: 'providerWithSchedule', organization: 'test-org1', catalogPath: '/catalog-info.yaml', @@ -202,6 +231,7 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: undefined, + allowForks: true, topic: { include: undefined, exclude: undefined, diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.ts index e888743258..a79a557ebf 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.ts @@ -32,6 +32,7 @@ export type GithubEntityProviderConfig = { repository?: RegExp; branch?: string; topic?: GithubTopicFilters; + allowForks?: boolean; }; validateLocationsExist: boolean; schedule?: TaskScheduleDefinition; @@ -72,6 +73,7 @@ function readProviderConfig( const host = config.getOptionalString('host') ?? 'github.com'; const repositoryPattern = config.getOptionalString('filters.repository'); const branchPattern = config.getOptionalString('filters.branch'); + const allowForks = config.getOptionalBoolean('filters.allowForks') ?? true; const topicFilterInclude = config?.getOptionalStringArray( 'filters.topic.include', ); @@ -103,6 +105,7 @@ function readProviderConfig( ? compileRegExp(repositoryPattern) : undefined, branch: branchPattern || undefined, + allowForks: allowForks, topic: { include: topicFilterInclude, exclude: topicFilterExclude, diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts index c24cd573e0..2711425998 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts @@ -181,7 +181,10 @@ describe('GithubOrgEntityProvider', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { - name: 'githubuser', + name: 'user-name', + annotations: { + 'github.com/user-login': 'githubuser', + }, }, spec: { memberOf: [], @@ -192,12 +195,13 @@ describe('GithubOrgEntityProvider', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { - name: 'githubuser', + name: 'user-name', annotations: { 'backstage.io/managed-by-location': 'url:https://github.com/githubuser', 'backstage.io/managed-by-origin-location': 'url:https://github.com/githubuser', + 'github.com/user-login': 'githubuser', }, }, spec: { @@ -211,7 +215,10 @@ describe('GithubOrgEntityProvider', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', metadata: { - name: 'mygroup', + name: 'group-name', + annotations: { + 'github.com/team-slug': 'backstage/mygroup', + }, }, spec: { type: 'team', @@ -223,12 +230,13 @@ describe('GithubOrgEntityProvider', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', metadata: { - name: 'mygroup', + name: 'group-name', annotations: { 'backstage.io/managed-by-location': 'url:https://github.com/orgs/backstage/teams/mygroup', 'backstage.io/managed-by-origin-location': 'url:https://github.com/orgs/backstage/teams/mygroup', + 'github.com/team-slug': 'backstage/mygroup', }, }, spec: { diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index 3093c2a897..2a888e6f91 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -65,6 +65,11 @@ import { getOrganizationTeam, getOrganizationTeamsFromUsers, } from '../lib/github'; +import { + ANNOTATION_GITHUB_TEAM_SLUG, + ANNOTATION_GITHUB_USER_LOGIN, +} from '../lib/annotation'; +import { splitTeamSlug } from '../lib/util'; /** * Options for {@link GithubOrgEntityProvider}. @@ -602,10 +607,21 @@ export function withLocations( org: string, entity: Entity, ): Entity { + const login = + entity.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] || + entity.metadata.name; + + let team = entity.metadata.name; + const slug = entity.metadata.annotations?.[ANNOTATION_GITHUB_TEAM_SLUG]; + if (slug) { + const [_, slugTeam] = splitTeamSlug(slug); + team = slugTeam; + } + const location = entity.kind === 'Group' - ? `url:${baseUrl}/orgs/${org}/teams/${entity.metadata.name}` - : `url:${baseUrl}/${entity.metadata.name}`; + ? `url:${baseUrl}/orgs/${org}/teams/${team}` + : `url:${baseUrl}/${login}`; return merge( { metadata: { diff --git a/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.test.ts index d8ebc8d0ad..7c08e8947d 100644 --- a/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/backend-tasks'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; import { githubEntityProviderCatalogModule } from './GithubEntityProviderCatalogModule'; import { GithubEntityProvider } from '../providers/GithubEntityProvider'; diff --git a/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.ts index d9dc0a12aa..246f700248 100644 --- a/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.ts @@ -19,11 +19,11 @@ import { coreServices, } from '@backstage/backend-plugin-api'; import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { GithubEntityProvider } from '../providers/GithubEntityProvider'; /** - * Registers the {@link GithubEntityProvider} with the catalog processing extension point. + * Registers the `GithubEntityProvider` with the catalog processing extension point. * * @alpha */ diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 95e436c122..67bb23d79e 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,88 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.1.14-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.13 + +### Patch Changes + +- 49948f644f: The config now consistently uses the term 'instance' to refer to a single GitLab API host. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 52c5685ceb: Implement Group and User Catalog Provider +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.13-next.2 + +### Patch Changes + +- 49948f644f: The config now consistently uses the term 'instance' to refer to a single GitLab API host. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.13-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 52c5685ceb: Implement Group and User Catalog Provider +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.1.11 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/alpha-api-report.md b/plugins/catalog-backend-module-gitlab/alpha-api-report.md new file mode 100644 index 0000000000..8912b4a7b2 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-gitlab" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const gitlabDiscoveryEntityProviderCatalogModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-gitlab/api-report.md b/plugins/catalog-backend-module-gitlab/api-report.md index 36713d2427..68ea45169f 100644 --- a/plugins/catalog-backend-module-gitlab/api-report.md +++ b/plugins/catalog-backend-module-gitlab/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; @@ -33,9 +32,6 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { refresh(logger: Logger): Promise<void>; } -// @alpha -export const gitlabDiscoveryEntityProviderCatalogModule: () => BackendFeature; - // @public export class GitLabDiscoveryProcessor implements CatalogProcessor { // (undocumented) @@ -55,4 +51,21 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { emit: CatalogProcessorEmit, ): Promise<boolean>; } + +// @public +export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { + // (undocumented) + connect(connection: EntityProviderConnection): Promise<void>; + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger; + schedule?: TaskRunner; + scheduler?: PluginTaskScheduler; + }, + ): GitlabOrgDiscoveryEntityProvider[]; + // (undocumented) + getProviderName(): string; +} ``` diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts index ac75a04c2a..00ad0ef032 100644 --- a/plugins/catalog-backend-module-gitlab/config.d.ts +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -31,7 +31,7 @@ export interface Config { host: string; /** * (Optional) Gitlab's group[/subgroup] where the discovery is done. - * If not defined the whole project will be scanned. + * If not defined the whole instance will be scanned. */ group?: string; /** @@ -48,6 +48,18 @@ export interface Config { * (Optional) TaskScheduleDefinition for the refresh. */ schedule?: TaskScheduleDefinitionConfig; + /** + * (Optional) RegExp for the Project Name Pattern + */ + projectPattern?: RegExp; + /** + * (Optional) RegExp for the User Name Pattern + */ + userPattern?: RegExp; + /** + * (Optional) RegExp for the Group Name Pattern + */ + groupPattern?: RegExp; } >; }; diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 79cf8fcb14..be78589199 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", "description": "A Backstage catalog backend module that helps integrate towards GitLab", - "version": "0.1.11", + "version": "0.1.14-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -54,10 +66,9 @@ "@types/lodash": "^4.14.151", "@types/uuid": "^8.0.0", "luxon": "^3.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts index b03c665ebc..756093bc56 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts @@ -193,7 +193,7 @@ export function parseUrl(urlString: string): { catalogPath: string; } { const url = new URL(urlString); - const path = url.pathname.substr(1).split('/'); + const path = url.pathname.slice(1).split('/'); // (/group/subgroup)/blob/branch|*/filepath const blobIndex = path.findIndex(p => p === 'blob'); diff --git a/plugins/catalog-backend-module-gitlab/src/alpha.ts b/plugins/catalog-backend-module-gitlab/src/alpha.ts new file mode 100644 index 0000000000..883ffc13c4 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { gitlabDiscoveryEntityProviderCatalogModule } from './service/GitlabDiscoveryEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-gitlab/src/index.ts b/plugins/catalog-backend-module-gitlab/src/index.ts index 27aac9e78f..2dd19c041d 100644 --- a/plugins/catalog-backend-module-gitlab/src/index.ts +++ b/plugins/catalog-backend-module-gitlab/src/index.ts @@ -21,5 +21,7 @@ */ export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; -export { GitlabDiscoveryEntityProvider } from './providers'; -export { gitlabDiscoveryEntityProviderCatalogModule } from './service/GitlabDiscoveryEntityProviderCatalogModule'; +export { + GitlabDiscoveryEntityProvider, + GitlabOrgDiscoveryEntityProvider, +} from './providers'; diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index adf0a78e8f..bad0eb2a86 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -20,14 +20,24 @@ import { GitLabIntegrationConfig, } from '@backstage/integration'; import { Logger } from 'winston'; +import { GitLabGroup, GitLabMembership, GitLabUser } from './types'; -export type ListOptions = { +export type CommonListOptions = { [key: string]: string | number | boolean | undefined; - group?: string; per_page?: number | undefined; page?: number | undefined; + active?: boolean; }; +interface ListProjectOptions extends CommonListOptions { + group?: string; +} + +interface UserListOptions extends CommonListOptions { + without_project_bots?: boolean | undefined; + exclude_internal?: boolean | undefined; +} + export type PagedResponse<T> = { items: T[]; nextPage?: number; @@ -49,7 +59,9 @@ export class GitLabClient { return this.config.host !== 'gitlab.com'; } - async listProjects(options?: ListOptions): Promise<PagedResponse<any>> { + async listProjects( + options?: ListProjectOptions, + ): Promise<PagedResponse<any>> { if (options?.group) { return this.pagedRequest( `/groups/${encodeURIComponent(options?.group)}/projects`, @@ -63,6 +75,53 @@ export class GitLabClient { return this.pagedRequest(`/projects`, options); } + async listUsers( + options?: UserListOptions, + ): Promise<PagedResponse<GitLabUser>> { + let requestOptions = options; + + if (!requestOptions) { + requestOptions = {}; + } + + requestOptions.without_project_bots = true; + requestOptions.exclude_internal = true; + + return this.pagedRequest(`/users?`, requestOptions); + } + + async listGroups( + options?: CommonListOptions, + ): Promise<PagedResponse<GitLabGroup>> { + return this.pagedRequest(`/groups`, options); + } + + async getUserMemberships(userId: number): Promise<GitLabMembership[]> { + const endpoint: string = `/users/${encodeURIComponent(userId)}/memberships`; + const request = new URL(`${this.config.apiBaseUrl}${endpoint}`); + request.searchParams.append('per_page', '100'); + + const response = await fetch(request.toString(), { + headers: getGitLabRequestOptions(this.config).headers, + method: 'GET', + }); + + if (!response.ok) { + if (response.status >= 500) { + this.logger.debug( + `Unexpected response when fetching ${request.toString()}. Expected 200 but got ${ + response.status + } - ${response.statusText}`, + ); + } + return []; + } + + return response.json().then(items => { + return items as GitLabMembership[]; + }); + } + /** * General existence check. * @@ -114,7 +173,7 @@ export class GitLabClient { */ async pagedRequest<T = any>( endpoint: string, - options?: ListOptions, + options?: CommonListOptions, ): Promise<PagedResponse<T>> { const request = new URL(`${this.config.apiBaseUrl}${endpoint}`); for (const key in options) { @@ -159,8 +218,8 @@ export class GitLabClient { * @param options - Initial ListOptions for the request function. */ export async function* paginated<T = any>( - request: (options: ListOptions) => Promise<PagedResponse<T>>, - options: ListOptions, + request: (options: CommonListOptions) => Promise<PagedResponse<T>>, + options: CommonListOptions, ) { let res; do { diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts index 86fe1aa385..c3fc8a4ede 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts @@ -31,12 +31,49 @@ export type GitLabProject = { path_with_namespace?: string; }; +export type GitLabUser = { + id: number; + username: string; + email: string; + name: string; + state: string; + web_url: string; + avatar_url: string; + groups?: GitLabGroup[]; +}; + +export type GitLabGroup = { + id: number; + name: string; + full_path: string; + description?: string; + parent_id?: number; +}; + +export type GitLabMembership = { + source_id: number; + source_name: string; + source_type: string; + access_level: number; +}; + export type GitlabProviderConfig = { host: string; group: string; id: string; - branch: string; + /** + * @deprecated use `fallbackBranch` instead + */ + branch?: string; + /** + * If there is no default branch defined at the project, this fallback is used to discover catalog files. + * Defaults to: `master` + */ + fallbackBranch: string; catalogFile: string; projectPattern: RegExp; + userPattern: RegExp; + groupPattern: RegExp; + orgEnabled?: boolean; schedule?: TaskScheduleDefinition; }; diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 1cc818c811..f0d88c34f5 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -61,7 +61,10 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { throw new Error('Either schedule or scheduler must be provided.'); } - const providerConfigs = readGitlabConfigs(config); + const providerConfigs = readGitlabConfigs( + config, + options.logger.child({ target: 'GitlabDiscoveryEntityProvider' }), + ); const integrations = ScmIntegrations.fromConfig(config).gitlab; const providers: GitlabDiscoveryEntityProvider[] = []; @@ -177,11 +180,15 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { continue; } - if (this.config.branch === '*' && project.default_branch === undefined) { + if ( + this.config.fallbackBranch === '*' && + project.default_branch === undefined + ) { continue; } - const project_branch = project.default_branch ?? this.config.branch; + const project_branch = + project.default_branch ?? this.config.fallbackBranch; const projectHasFile: boolean = await client.hasFile( project.path_with_namespace ?? '', @@ -204,7 +211,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { } private createLocationSpec(project: GitLabProject): LocationSpec { - const project_branch = project.default_branch ?? this.config.branch; + const project_branch = project.default_branch ?? this.config.fallbackBranch; return { type: 'url', target: `${project.web_url}/-/blob/${project_branch}/${this.config.catalogFile}`, diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts new file mode 100644 index 0000000000..74649f41da --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts @@ -0,0 +1,514 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { + PluginTaskScheduler, + TaskInvocationDefinition, + TaskRunner, +} from '@backstage/backend-tasks'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { GitlabOrgDiscoveryEntityProvider } from './GitlabOrgDiscoveryEntityProvider'; + +class PersistingTaskRunner implements TaskRunner { + private tasks: TaskInvocationDefinition[] = []; + + getTasks() { + return this.tasks; + } + + run(task: TaskInvocationDefinition): Promise<void> { + this.tasks.push(task); + return Promise.resolve(undefined); + } +} + +const logger = getVoidLogger(); + +const server = setupServer(); + +describe('GitlabOrgDiscoveryEntityProvider', () => { + setupRequestMockHandlers(server); + afterEach(() => jest.resetAllMocks()); + + it('no provider config', () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({}); + const providers = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + }); + + expect(providers).toHaveLength(0); + }); + + it('single simple discovery config with org disabled', () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + }, + }, + }, + }, + }); + const providers = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + }); + + expect(providers).toHaveLength(0); + }); + + it('single simple discovery config with org enabled', () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + orgEnabled: true, + }, + }, + }, + }, + }); + const providers = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + }); + + expect(providers).toHaveLength(1); + expect(providers[0].getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + }); + + it('multiple discovery configs', () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + orgEnabled: true, + }, + 'second-test': { + host: 'test-gitlab', + group: 'second-group', + orgEnabled: true, + }, + }, + }, + }, + }); + const providers = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + }); + + expect(providers).toHaveLength(2); + expect(providers[0].getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + expect(providers[1].getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:second-test', + ); + }); + + it('apply full update on scheduled execution', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + orgEnabled: true, + }, + }, + }, + }, + }); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + server.use( + rest.get( + `https://api.gitlab.example/api/v4/groups/test-group/projects`, + (_req, res, ctx) => { + const response = [ + { + id: 123, + default_branch: 'master', + archived: false, + last_activity_at: new Date().toString(), + web_url: 'https://api.gitlab.example/test-group/test-repo', + path_with_namespace: 'test-group/test-repo', + }, + ]; + return res(ctx.json(response)); + }, + ), + rest.get(`https://api.gitlab.example/api/v4/users`, (_req, res, ctx) => { + const response = [ + { + id: 1, + username: 'test1', + name: 'Test Testit', + state: 'active', + avatar_url: 'https://secure.gravatar.com/', + web_url: 'https://gitlab.example/test1', + created_at: '2023-01-19T07:27:03.333Z', + bio: '', + location: null, + public_email: null, + skype: '', + linkedin: '', + twitter: '', + website_url: '', + organization: null, + job_title: '', + pronouns: null, + bot: false, + work_information: null, + followers: 0, + following: 0, + is_followed: false, + local_time: null, + last_sign_in_at: '2023-01-19T07:27:49.601Z', + confirmed_at: '2023-01-19T07:27:02.905Z', + last_activity_on: '2023-01-19', + email: 'test@example.com', + theme_id: 1, + color_scheme_id: 1, + projects_limit: 100000, + current_sign_in_at: '2023-01-19T09:09:10.676Z', + identities: [], + can_create_group: true, + can_create_project: true, + two_factor_enabled: false, + external: false, + private_profile: false, + commit_email: 'test@example.com', + is_admin: false, + note: '', + }, + ]; + return res(ctx.json(response)); + }), + rest.get(`https://api.gitlab.example/api/v4/groups`, (_req, res, ctx) => { + const response = [ + { + id: 1, + web_url: 'https://gitlab.example/groups/group1', + name: 'group1', + path: 'group1', + description: '', + visibility: 'internal', + share_with_group_lock: false, + require_two_factor_authentication: false, + two_factor_grace_period: 48, + project_creation_level: 'developer', + auto_devops_enabled: null, + subgroup_creation_level: 'owner', + emails_disabled: null, + mentions_disabled: null, + lfs_enabled: true, + default_branch_protection: 2, + avatar_url: null, + request_access_enabled: false, + full_name: '8020', + full_path: '8020', + created_at: '2017-06-19T06:42:34.160Z', + parent_id: null, + }, + { + id: 2, + web_url: 'https://gitlab.example/groups/group1/group2', + name: 'group2', + path: 'group1/group2', + description: 'Group2', + visibility: 'internal', + share_with_group_lock: false, + require_two_factor_authentication: false, + two_factor_grace_period: 48, + project_creation_level: 'developer', + auto_devops_enabled: null, + subgroup_creation_level: 'owner', + emails_disabled: null, + mentions_disabled: null, + lfs_enabled: true, + request_access_enabled: false, + full_name: 'group2', + full_path: 'group1/group2', + created_at: '2017-12-07T13:20:40.675Z', + parent_id: null, + }, + ]; + return res(ctx.json(response)); + }), + rest.get( + `https://api.gitlab.example/api/v4/users/1/memberships`, + (_req, res, ctx) => { + const response = [ + { + source_id: 2, + source_name: 'Group 2', + source_type: 'Namespace', + access_level: 50, + }, + ]; + return res(ctx.json(response)); + }, + ), + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise<void>)(); + + const expectedEntities = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': 'url:test-gitlab/test1', + 'backstage.io/managed-by-origin-location': + 'url:test-gitlab/test1', + 'test-gitlab/user-login': 'https://gitlab.example/test1', + }, + name: 'test1', + }, + spec: { + memberOf: ['group1-group2'], + profile: { + displayName: 'Test Testit', + email: 'test@example.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:test-gitlab/teams/group1-group2', + 'backstage.io/managed-by-origin-location': + 'url:test-gitlab/teams/group1-group2', + 'test-gitlab/team-path': 'group1/group2', + }, + description: 'Group2', + name: 'group1-group2', + }, + spec: { + children: [], + profile: { + displayName: 'group2', + }, + type: 'team', + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + ]; + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: expectedEntities, + }); + }); + + it('fail without schedule and scheduler', () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + orgEnabled: true, + }, + }, + }, + }, + }); + + expect(() => + GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + }), + ).toThrow('Either schedule or scheduler must be provided'); + }); + + it('fail with scheduler but no schedule config', () => { + const scheduler = { + createScheduledTaskRunner: (_: any) => jest.fn(), + } as unknown as PluginTaskScheduler; + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + orgEnabled: true, + }, + }, + }, + }, + }); + + expect(() => + GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + scheduler, + }), + ).toThrow( + 'No schedule provided neither via code nor config for GitlabOrgDiscoveryEntityProvider:test-id', + ); + }); + + it('single simple provider config with schedule in config', async () => { + const schedule = new PersistingTaskRunner(); + const scheduler = { + createScheduledTaskRunner: (_: any) => schedule, + } as unknown as PluginTaskScheduler; + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + orgEnabled: true, + schedule: { + frequency: 'PT30M', + timeout: 'PT3M', + }, + }, + }, + }, + }, + }); + const providers = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + scheduler, + }); + + expect(providers).toHaveLength(1); + expect(providers[0].getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + }); +}); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts new file mode 100644 index 0000000000..ad9d9dae79 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts @@ -0,0 +1,378 @@ +/* + * 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 { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; +import { Config } from '@backstage/config'; +import { GitLabIntegration, ScmIntegrations } from '@backstage/integration'; +import { + EntityProvider, + EntityProviderConnection, +} from '@backstage/plugin-catalog-backend'; +import * as uuid from 'uuid'; +import { Logger } from 'winston'; +import { + GitLabClient, + GitlabProviderConfig, + paginated, + readGitlabConfigs, +} from '../lib'; +import { GitLabGroup, GitLabUser } from '../lib/types'; +import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, + Entity, + UserEntity, + GroupEntity, +} from '@backstage/catalog-model'; +import { merge } from 'lodash'; + +type Result = { + scanned: number; + matches: GitLabUser[]; +}; + +type GroupResult = { + scanned: number; + matches: GitLabGroup[]; +}; + +/** + * Discovers users and groups from a Gitlab instance. + * @public + */ +export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { + private readonly config: GitlabProviderConfig; + private readonly integration: GitLabIntegration; + private readonly logger: Logger; + private readonly scheduleFn: () => Promise<void>; + private connection?: EntityProviderConnection; + + static fromConfig( + config: Config, + options: { + logger: Logger; + schedule?: TaskRunner; + scheduler?: PluginTaskScheduler; + }, + ): GitlabOrgDiscoveryEntityProvider[] { + if (!options.schedule && !options.scheduler) { + throw new Error('Either schedule or scheduler must be provided.'); + } + + const providerConfigs = readGitlabConfigs( + config, + options.logger.child({ target: 'GitlabOrgDiscoveryEntityProvider' }), + ); + const integrations = ScmIntegrations.fromConfig(config).gitlab; + const providers: GitlabOrgDiscoveryEntityProvider[] = []; + + providerConfigs.forEach(providerConfig => { + const integration = integrations.byHost(providerConfig.host); + + if (!providerConfig.orgEnabled) { + return; + } + + if (!integration) { + throw new Error( + `No gitlab integration found that matches host ${providerConfig.host}`, + ); + } + + if (!options.schedule && !providerConfig.schedule) { + throw new Error( + `No schedule provided neither via code nor config for GitlabOrgDiscoveryEntityProvider:${providerConfig.id}.`, + ); + } + + const taskRunner = + options.schedule ?? + options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!); + + providers.push( + new GitlabOrgDiscoveryEntityProvider({ + ...options, + config: providerConfig, + integration, + taskRunner, + }), + ); + }); + return providers; + } + + private constructor(options: { + config: GitlabProviderConfig; + integration: GitLabIntegration; + logger: Logger; + taskRunner: TaskRunner; + }) { + this.config = options.config; + this.integration = options.integration; + this.logger = options.logger.child({ + target: this.getProviderName(), + }); + this.scheduleFn = this.createScheduleFn(options.taskRunner); + } + + getProviderName(): string { + return `GitlabOrgDiscoveryEntityProvider:${this.config.id}`; + } + + async connect(connection: EntityProviderConnection): Promise<void> { + this.connection = connection; + await this.scheduleFn(); + } + + private createScheduleFn(taskRunner: TaskRunner): () => Promise<void> { + return async () => { + const taskId = `${this.getProviderName()}:refresh`; + return taskRunner.run({ + id: taskId, + fn: async () => { + const logger = this.logger.child({ + class: GitlabOrgDiscoveryEntityProvider.prototype.constructor.name, + taskId, + taskInstanceId: uuid.v4(), + }); + + try { + await this.refresh(logger); + } catch (error) { + logger.error(`${this.getProviderName()} refresh failed`, error); + } + }, + }); + }; + } + + private async refresh(logger: Logger): Promise<void> { + if (!this.connection) { + throw new Error( + `Gitlab discovery connection not initialized for ${this.getProviderName()}`, + ); + } + + const client = new GitLabClient({ + config: this.integration.config, + logger: logger, + }); + + const users = paginated<GitLabUser>(options => client.listUsers(options), { + page: 1, + per_page: 100, + active: true, + }); + + const groups = paginated<GitLabGroup>( + options => client.listGroups(options), + { + page: 1, + per_page: 100, + }, + ); + + const idMappedGroup: { [groupId: number]: GitLabGroup } = {}; + + const res: Result = { + scanned: 0, + matches: [], + }; + + const groupRes: GroupResult = { + scanned: 0, + matches: [], + }; + + for await (const group of groups) { + if (!this.config.groupPattern.test(group.full_path ?? '')) { + continue; + } + + groupRes.scanned++; + groupRes.matches.push(group); + + idMappedGroup[group.id] = group; + } + + for await (const user of users) { + if (!this.config.userPattern.test(user.email ?? user.username ?? '')) { + continue; + } + + res.scanned++; + + if (user.state !== 'active') { + continue; + } + + const memberships = await client.getUserMemberships(user.id); + const userGroups: GitLabGroup[] = []; + + for (const i of memberships) { + if ( + i.source_type === 'Namespace' && + idMappedGroup.hasOwnProperty(i.source_id) + ) { + userGroups.push(idMappedGroup[i.source_id]); + } + } + + user.groups = userGroups; + + res.matches.push(user); + } + + const groupsWithUsers = groupRes.matches.filter(group => { + return ( + res.matches.filter(x => { + return !!x.groups?.find(y => y.id === group.id); + }).length > 0 + ); + }); + + const userEntities = res.matches.map(p => + this.createUserEntity(p, this.integration.config.host), + ); + const groupEntities = this.createGroupEntities( + groupsWithUsers, + this.integration.config.host, + ); + + await this.connection.applyMutation({ + type: 'full', + entities: [...userEntities, ...groupEntities].map(entity => ({ + locationKey: this.getProviderName(), + entity: this.withLocations(this.integration.config.host, entity), + })), + }); + } + + private createGroupEntities( + groupResult: GitLabGroup[], + host: string, + ): GroupEntity[] { + const idMapped: { [groupId: number]: GitLabGroup } = {}; + const entities: GroupEntity[] = []; + + for (const group of groupResult) { + idMapped[group.id] = group; + } + + for (const group of groupResult) { + const entity = this.createGroupEntity(group, host); + + if (group.parent_id && idMapped.hasOwnProperty(group.parent_id)) { + entity.spec.parent = idMapped[group.parent_id].full_path; + } + + entities.push(entity); + } + + return entities; + } + + private withLocations(host: string, entity: Entity): Entity { + const location = + entity.kind === 'Group' + ? `url:${host}/teams/${entity.metadata.name}` + : `url:${host}/${entity.metadata.name}`; + return merge( + { + metadata: { + annotations: { + [ANNOTATION_LOCATION]: location, + [ANNOTATION_ORIGIN_LOCATION]: location, + }, + }, + }, + entity, + ) as Entity; + } + + private createUserEntity(user: GitLabUser, host: string): UserEntity { + const annotations: { [annotationName: string]: string } = {}; + + annotations[`${host}/user-login`] = user.web_url; + + const entity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: user.username, + annotations: annotations, + }, + spec: { + profile: { + displayName: user.name, + picture: user.avatar_url, + }, + memberOf: [], + }, + }; + + if (user.email) { + if (!entity.spec) { + entity.spec = {}; + } + + if (!entity.spec.profile) { + entity.spec.profile = {}; + } + + entity.spec.profile.email = user.email; + } + + if (user.groups) { + for (const group of user.groups) { + if (!entity.spec.memberOf) { + entity.spec.memberOf = []; + } + entity.spec.memberOf.push(group.full_path.replace('/', '-')); + } + } + + return entity; + } + + private createGroupEntity(group: GitLabGroup, host: string): GroupEntity { + const annotations: { [annotationName: string]: string } = {}; + + annotations[`${host}/team-path`] = group.full_path; + + const entity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: group.full_path.replace('/', '-'), + annotations: annotations, + }, + spec: { + type: 'team', + children: [], + profile: { + displayName: group.name, + }, + }, + }; + + if (group.description) { + entity.metadata.description = group.description; + } + + return entity; + } +} diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index 6b337bf7cd..c9b35e78c2 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -17,6 +17,9 @@ import { ConfigReader } from '@backstage/config'; import { Duration } from 'luxon'; import { readGitlabConfigs } from './config'; +import { getVoidLogger } from '@backstage/backend-common'; + +const logger = getVoidLogger(); describe('config', () => { it('empty gitlab config', () => { @@ -26,7 +29,7 @@ describe('config', () => { }, }); - const result = readGitlabConfigs(config); + const result = readGitlabConfigs(config, logger); expect(result).toHaveLength(0); }); @@ -44,16 +47,19 @@ describe('config', () => { }, }); - const result = readGitlabConfigs(config); + const result = readGitlabConfigs(config, logger); expect(result).toHaveLength(1); result.forEach(r => expect(r).toStrictEqual({ id: 'test', group: 'group', - branch: 'master', + fallbackBranch: 'master', host: 'host', catalogFile: 'catalog-info.yaml', projectPattern: /[\s\S]*/, + groupPattern: /[\s\S]*/, + userPattern: /[\s\S]*/, + orgEnabled: false, schedule: undefined, }), ); @@ -75,16 +81,19 @@ describe('config', () => { }, }); - const result = readGitlabConfigs(config); + const result = readGitlabConfigs(config, logger); expect(result).toHaveLength(1); result.forEach(r => expect(r).toStrictEqual({ id: 'test', group: 'group', - branch: 'not-master', + fallbackBranch: 'not-master', host: 'host', catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, + groupPattern: /[\s\S]*/, + userPattern: /[\s\S]*/, + orgEnabled: false, schedule: undefined, }), ); @@ -110,16 +119,19 @@ describe('config', () => { }, }); - const result = readGitlabConfigs(config); + const result = readGitlabConfigs(config, logger); expect(result).toHaveLength(1); result.forEach(r => expect(r).toStrictEqual({ id: 'test', group: 'group', - branch: 'master', + fallbackBranch: 'master', host: 'host', catalogFile: 'catalog-info.yaml', projectPattern: /[\s\S]*/, + groupPattern: /[\s\S]*/, + userPattern: /[\s\S]*/, + orgEnabled: false, schedule: { frequency: Duration.fromISO('PT30M'), timeout: { @@ -146,7 +158,7 @@ describe('config', () => { }, }); - expect(() => readGitlabConfigs(config)).toThrow( + expect(() => readGitlabConfigs(config, logger)).toThrow( "Missing required config value at 'catalog.providers.gitlab.test.host'", ); }); @@ -165,7 +177,7 @@ describe('config', () => { }, }); - const result = readGitlabConfigs(config); + const result = readGitlabConfigs(config, logger); expect(result).toHaveLength(1); expect(result[0].group).toEqual(''); }); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts index 3dfc00745e..e5cf5a6298 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -17,6 +17,7 @@ import { readTaskScheduleDefinitionFromConfig } from '@backstage/backend-tasks'; import { Config } from '@backstage/config'; import { GitlabProviderConfig } from '../lib'; +import { Logger } from 'winston'; /** * Extracts the gitlab config from a config object @@ -25,16 +26,37 @@ import { GitlabProviderConfig } from '../lib'; * * @param id - The provider key * @param config - The config object to extract from + * @param logger - The logger */ -function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { +function readGitlabConfig( + id: string, + config: Config, + logger: Logger, +): GitlabProviderConfig { const group = config.getOptionalString('group') ?? ''; const host = config.getString('host'); - const branch = config.getOptionalString('branch') ?? 'master'; + const branch = config.getOptionalString('branch'); + + if (branch) { + logger.warn( + 'The configuration key `branch` has been deprecated in favor of the configuration key `fallbackBranch`.', + ); + } + + const fallbackBranch = + config.getOptionalString('fallbackBranch') ?? branch ?? 'master'; const catalogFile = config.getOptionalString('entityFilename') ?? 'catalog-info.yaml'; const projectPattern = new RegExp( config.getOptionalString('projectPattern') ?? /[\s\S]*/, ); + const userPattern = new RegExp( + config.getOptionalString('userPattern') ?? /[\s\S]*/, + ); + const groupPattern = new RegExp( + config.getOptionalString('groupPattern') ?? /[\s\S]*/, + ); + const orgEnabled: boolean = config.getOptionalBoolean('orgEnabled') ?? false; const schedule = config.has('schedule') ? readTaskScheduleDefinitionFromConfig(config.getConfig('schedule')) @@ -43,11 +65,14 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { return { id, group, - branch, + fallbackBranch, host, catalogFile, projectPattern, + userPattern, + groupPattern, schedule, + orgEnabled, }; } @@ -57,8 +82,12 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { * @public * * @param config - The config object to extract from + * @param logger - The logger */ -export function readGitlabConfigs(config: Config): GitlabProviderConfig[] { +export function readGitlabConfigs( + config: Config, + logger: Logger, +): GitlabProviderConfig[] { const configs: GitlabProviderConfig[] = []; const providerConfigs = config.getOptionalConfig('catalog.providers.gitlab'); @@ -68,7 +97,13 @@ export function readGitlabConfigs(config: Config): GitlabProviderConfig[] { } for (const id of providerConfigs.keys()) { - configs.push(readGitlabConfig(id, providerConfigs.getConfig(id))); + configs.push( + readGitlabConfig( + id, + providerConfigs.getConfig(id), + logger.child({ target: id }), + ), + ); } return configs; diff --git a/plugins/catalog-backend-module-gitlab/src/providers/index.ts b/plugins/catalog-backend-module-gitlab/src/providers/index.ts index e7cb00a73f..2091b3f47a 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/index.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/index.ts @@ -15,3 +15,4 @@ */ export { GitlabDiscoveryEntityProvider } from './GitlabDiscoveryEntityProvider'; +export { GitlabOrgDiscoveryEntityProvider } from './GitlabOrgDiscoveryEntityProvider'; diff --git a/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.test.ts index ca8d86f555..efb86e4223 100644 --- a/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/backend-tasks'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; import { gitlabDiscoveryEntityProviderCatalogModule } from './GitlabDiscoveryEntityProviderCatalogModule'; import { GitlabDiscoveryEntityProvider } from '../providers'; diff --git a/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.ts index bb4fb656a8..0adf5b18f2 100644 --- a/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.ts @@ -19,7 +19,7 @@ import { coreServices, } from '@backstage/backend-plugin-api'; import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { GitlabDiscoveryEntityProvider } from '../providers'; /** diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 10ac2388d7..88ae72e1a8 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,104 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.2.2-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- bf611cf019: Fix missing `dependencies` in `package.json` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-defaults@0.1.8-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## 0.2.0 + +### Minor Changes + +- 1ba120faa3: Added new mechanism to handle deltas in incremental providers + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 407dc01fc9: Removing extra imports for `run` script as `TestBackend` auto loads the default factories +- b7e36660d5: Return `EventSubscriber` from `addIncrementalEntityProvider` to hook up to `EventsBackend` +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- 77c41b6924: Updated README to include newer API options for incremental entity providers +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## 0.2.0-next.2 + +### Patch Changes + +- 407dc01fc9: Removing extra imports for `run` script as `TestBackend` auto loads the default factories +- 77c41b6924: Updated README to include newer API options for incremental entity providers +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## 0.2.0-next.1 + +### Patch Changes + +- b7e36660d5: Return `EventSubscriber` from `addIncrementalEntityProvider` to hook up to `EventsBackend` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## 0.2.0-next.0 + +### Minor Changes + +- 1ba120faa3: Added new mechanism to handle deltas in incremental providers + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.1.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/README.md b/plugins/catalog-backend-module-incremental-ingestion/README.md index 0a646e4857..f85321e613 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/README.md +++ b/plugins/catalog-backend-module-incremental-ingestion/README.md @@ -4,7 +4,7 @@ The Incremental Ingestion catalog backend module provides an Incremental Entity ## Why did we create it? -Backstage provides an [Entity Provider mechanism that has two kinds of mutations](https://backstage.io/docs/features/software-catalog/external-integrations#provider-mutations): `delta` and `full`. `delta` mutations tell Backstage Catalog which entities should be added and removed from the catalog. `full` mutation accepts a list of entities and automatically computes which entities must be removed by comparing the provided entities against existing entities to create a diff between the two sets. These two kinds of mutations are convenient for different kinds of data sources. A `delta` mutation can be used with a data source that emits UPDATE and DELETE events for its data. A `full` mutation is useful for APIs that produce fewer entities than can fit in Backstage processes' memory. +Backstage provides an [Entity Provider mechanism that has two kinds of mutations](https://backstage.io/docs/features/software-catalog/external-integrations#provider-mutations): `delta` and `full`. `delta` mutations tell the Backstage Catalog which entities should be added and removed from the catalog. `full` mutations accept a list of entities and automatically computes which entities must be removed by comparing the provided entities against existing entities to create a diff between the two sets. These two kinds of mutations are convenient for different kinds of data sources. A `delta` mutation can be used with a data source that emits UPDATE and DELETE events for its data. A `full` mutation is useful for APIs that produce fewer entities than can fit in the Backstage processes' memory. Unfortunately, these two kinds of mutations are insufficient for very large data sources for the following reasons, @@ -41,69 +41,82 @@ The Incremental Entity Provider backend is designed for data sources that provid ## Installation -1. Install `@backstage/plugin-catalog-backend-module-incremental-ingestion` with `yarn workspace backend add @backstage/plugin-catalog-backend-module-incremental-ingestion` -2. Import `IncrementalCatalogBuilder` from `@backstage/plugin-catalog-backend-module-incremental-ingestion` and instantiate it with `await IncrementalCatalogBuilder.create(env, builder)`. You have to pass `builder` into `IncrementalCatalogBuilder.create` function because `IncrementalCatalogBuilder` will convert an `IncrementalEntityProvider` into an `EntityProvider` and call `builder.addEntityProvider`. +1. Install `@backstage/plugin-catalog-backend-module-incremental-ingestion` with `yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-incremental-ingestion` from the Backstage root directory. +2. In your catalog.ts, import `IncrementalCatalogBuilder` from `@backstage/plugin-catalog-backend-module-incremental-ingestion` and instantiate it with `await IncrementalCatalogBuilder.create(env, builder)`. You have to pass `builder` into `IncrementalCatalogBuilder.create` function because `IncrementalCatalogBuilder` will convert an `IncrementalEntityProvider` into an `EntityProvider` and call `builder.addEntityProvider`. - ```ts - const builder = CatalogBuilder.create(env); - // incremental builder receives builder because it'll register - // incremental entity providers with the builder - const incrementalBuilder = await IncrementalCatalogBuilder.create( - env, - builder, - ); - ``` +```ts +const builder = CatalogBuilder.create(env); +// incremental builder receives builder because it'll register +// incremental entity providers with the builder +const incrementalBuilder = await IncrementalCatalogBuilder.create(env, builder); +``` -3. Last step, add `await incrementBuilder.build()` after `await builder.build()` to ensure that all `CatalogBuilder` migration run before running `incrementBuilder.build()` migrations. +3. After building the regular `CatalogBuilder`, build the incremental builder: - ```ts - const { processingEngine, router } = await builder.build(); +```ts +// Must be run first to ensure CatalogBuilder database migrations run before Incremental Entity Provider database migrations +const { processingEngine, router } = await builder.build(); - // this has to run after `await builder.build()` to ensure that catalog migrations are completed - // before incremental builder migrations are executed - await incrementalBuilder.build(); - ``` +// Returns an optional - but highly recommended - set of administrative routes +const { incrementalAdminRouter } = await incrementBuilder.build(); +``` - The result should look something like this, +The final result should look something like this, - ```ts - import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; - import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; - import { IncrementalCatalogBuilder } from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; - import { Router } from 'express'; - import { Duration } from 'luxon'; - import { PluginEnvironment } from '../types'; +```ts +import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; +import { IncrementalCatalogBuilder } from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; +import { Router } from 'express'; +import { Duration } from 'luxon'; +import { PluginEnvironment } from '../types'; - export default async function createPlugin( - env: PluginEnvironment, - ): Promise<Router> { - const builder = CatalogBuilder.create(env); - // incremental builder receives builder because it'll register - // incremental entity providers with the builder - const incrementalBuilder = await IncrementalCatalogBuilder.create( - env, - builder, - ); +export default async function createPlugin( + env: PluginEnvironment, +): Promise<Router> { + const builder = CatalogBuilder.create(env); + // incremental builder receives builder because it'll register + // incremental entity providers with the builder + const incrementalBuilder = await IncrementalCatalogBuilder.create( + env, + builder, + ); - builder.addProcessor(new ScaffolderEntitiesProcessor()); + builder.addProcessor(new ScaffolderEntitiesProcessor()); - const { processingEngine, router } = await builder.build(); + const { processingEngine, router } = await builder.build(); + const { incrementalAdminRouter } = await incrementalBuilder.build(); - // this has to run after `await builder.build()` so ensure that catalog migrations are completed - // before incremental builder migrations are executed - const { incrementalAdminRouter } = await incrementalBuilder.build(); + router.use(incrementalAdminRouter); - router.use('/incremental', incrementalAdminRouter); + await processingEngine.start(); - await processingEngine.start(); + return router; +} +``` - return router; - } - ``` +## Administrative Routes + +If you want to manage your incremental entity providers via REST endpoints, the following endpoints are available: + +| Method | Path | Description | +| ------ | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | +| GET | `/incremental/health` | Checks the health of all incremental providers. Returns array of any unhealthy ones. | +| GET | `/incremental/providers` | Get a list of all known incremental entity providers | +| GET | `/incremental/providers/:provider` | Checks the status of an incremental provider (resting, interstitial, etc). | +| POST | `/incremental/providers/:provider/trigger` | Triggers a provider's next action immediately. E.g., if it's currently interstitial, it will trigger the next burst. | +| POST | `/incremental/providers/:provider/start` | Stop the current ingestion cycle and start a new one immediately. | +| POST | `/incremental/providers/:provider/cancel` | Stop the current ingestion cycle and start a new one in 24 hours. | +| DELETE | `/incremental/providers/:provider` | Completely remove all records for the provider and schedule it to start again in 24 hours. | +| GET | `/incremental/providers/:provider/marks` | Retrieve a list of all ingestion marks for the current ingestion cycle. | +| DELETE | `/incremental/providers/:provider/marks` | Remove all ingestion marks for the current ingestion cycle. | +| POST | `/incremental/cleanup` | Completely remove all records for ALL providers and schedule them to start again in 24 hours. (CAUTION! Can cause orphans!) | + +In all cases, `:provider` is the name of the incremental entity provider. ## Writing an Incremental Entity Provider -To create an Incremental Entity Provider, you need to know how to retrieve a single page of the data that you wish to ingest into the Backstage catalog. If the API has pagination and you know how to make a paginated request to that API, you'll be able to implement an Incremental Entity Provider for this API. For more information about compatibility, checkout <a href="#compatible-data-source">Compatible data sources</a> section on this page. +To create an Incremental Entity Provider, you need to know how to retrieve a single page of the data that you wish to ingest into the Backstage catalog. If the API has pagination and you know how to make a paginated request to that API, you'll be able to implement an Incremental Entity Provider for this API. For more information about compatibility, check out the <a href="#requirements">requirements</a> section of this page. Here is the type definition for an Incremental Entity Provider. @@ -114,7 +127,19 @@ interface IncrementalEntityProvider<TCursor, TContext> { * operating in the catalog. */ getProviderName(): string; - + /** + * Return a single page of entities from a specific point in the + * ingestion. + * + * @param context - anything needed in order to fetch a single page. + * @param cursor - a unique value identifying the page to ingest. + * @returns The entities to be ingested, as well as the cursor of + * the next page after this one. + */ + next( + context: TContext, + cursor?: TCursor, + ): Promise<EntityIteratorResult<TCursor>>; /** * Do any setup and teardown necessary in order to provide the * context for fetching pages. This should always invoke `burst` in @@ -123,24 +148,10 @@ interface IncrementalEntityProvider<TCursor, TContext> { * @param burst - a function which performs a series of iterations */ around(burst: (context: TContext) => Promise<void>): Promise<void>; - - /** - * Return a single page of entities from a specific point in the - * ingestion. - * - * @param context - anything needed in order to fetch a single page. - * @param cursor - a unique value identifying the page to ingest. - * @returns the entities to be ingested, as well as the cursor of - * the the next page after this one. - */ - next( - context: TContext, - cursor?: TCursor, - ): Promise<EntityIteratorResult<TCursor>>; } ``` -For tutorial, we'll write an Incremental Entity Provider that will call an imaginary API. This imaginary API will return a list of imaginary services. This imaginary API has an imaginary API client with the following interface. +For this tutorial, we'll write an Incremental Entity Provider that will call an imaginary API. This imaginary API will return a list of imaginary services. The imaginary API has an imaginary API client with the following interface. ```ts interface MyApiClient { @@ -157,15 +168,12 @@ interface Service { } ``` -These are the only 3 methods that you need to implement. `getProviderName()` is pretty self explanatory and it's exactly same as on Entity Provider. +These are the only 3 methods that you need to implement. `getProviderName()` is pretty self explanatory and it's identical to the `getProviderName()` method on a regular Entity Provider. ```ts -import { - IncrementalEntityProvider, - EntityIteratorResult, -} from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; +import { IncrementalEntityProvider } from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; -// this will include your pagination information, let's say our API accepts a `page` parameter. +// This will include your pagination information, let's say our API accepts a `page` parameter. // In this case, the cursor will include `page` interface MyApiCursor { page: number; @@ -200,7 +208,7 @@ export class MyIncrementalEntityProvider await burst({ apiClient }); - // if you need to do any teardown, you can do it here + // If you need to do any teardown, you can do it here. } } ``` @@ -306,31 +314,45 @@ We'll assume you followed the <a href="#installation">Installation</a> instructi ```ts const incrementalBuilder = await IncrementalCatalogBuilder.create(env, builder); -// I'm assuming you're going to get your token from config +// Assuming the token for the API comes from config const token = config.getString('myApiClient.token'); -const myEntityProvider = new MyIncrementalEntityProvider(token) +const myEntityProvider = new MyIncrementalEntityProvider(token); -incrementalBuilder.addIncrementalEntityProvider( - myEntityProvider, - { - // how long should it attempt to read pages from the API - // keep this short. Incremental Entity Provider will attempt to - // read as many pages as it can in this time - burstLength: Duration.fromObject({ seconds: 3 }), - // how long should it wait between bursts? - burstInterval: Duration.fromObject({ seconds: 3 }), - // how long should it rest before re-ingesting again? - restLength: Duration.fromObject({ day: 1 }) - // optional back-off configuration - how long should it wait to retry? - backoff: [ - Duration.fromObject({ seconds: 5 }), - Duration.fromObject({ seconds: 30 }), - Duration.fromObject({ minutes: 10 }), - Duration.fromObject({ hours: 3 }) - ] - } -) +incrementalBuilder.addIncrementalEntityProvider(myEntityProvider, { + // How long should it attempt to read pages from the API in a + // single burst? Keep this short. The Incremental Entity Provider + // will attempt to read as many pages as it can in this time + burstLength: Duration.fromObject({ seconds: 3 }), + + // How long should it wait between bursts? + burstInterval: Duration.fromObject({ seconds: 3 }), + + // How long should it rest before re-ingesting again? + restLength: Duration.fromObject({ day: 1 }), + + // Optional back-off configuration - how long should it wait to retry + // in the event of an error? + backoff: [ + Duration.fromObject({ seconds: 5 }), + Duration.fromObject({ seconds: 30 }), + Duration.fromObject({ minutes: 10 }), + Duration.fromObject({ hours: 3 }), + ], + + // Optional. Use this to prevent removal of entities above a given + // percentage. This can be helpful if a data source is flaky and + // sometimes returns a successful status, but fewer than expected + // assets to add or maintain in the catalog. + rejectRemovalsAbovePercentage: 5, + + // Optional. Similar to rejectRemovalsAbovePercentage, except it + // applies to complete, 100% failure of a data source. If true, + // a data source that returns a successful status but does not + // provide any assets to turn into entities will have its empty + // data set rejected. + rejectEmptySourceCollections: true, +}); ``` That's it!!! diff --git a/plugins/catalog-backend-module-incremental-ingestion/alpha-api-report.md b/plugins/catalog-backend-module-incremental-ingestion/alpha-api-report.md new file mode 100644 index 0000000000..ec9c59a4c4 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/alpha-api-report.md @@ -0,0 +1,19 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-incremental-ingestion" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { IncrementalEntityProvider } from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; +import { IncrementalEntityProviderOptions } from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; + +// @alpha +export const incrementalIngestionEntityProviderCatalogModule: (options: { + providers: { + provider: IncrementalEntityProvider<unknown, unknown>; + options: IncrementalEntityProviderOptions; + }[]; +}) => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-incremental-ingestion/api-report.md b/plugins/catalog-backend-module-incremental-ingestion/api-report.md index 387e505c33..167c07f70f 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/api-report.md +++ b/plugins/catalog-backend-module-incremental-ingestion/api-report.md @@ -5,11 +5,12 @@ ```ts /// <reference types="express" /> -import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; import type { Config } from '@backstage/config'; import type { DeferredEntity } from '@backstage/plugin-catalog-backend'; import type { DurationObjectUnits } from 'luxon'; +import { EventParams } from '@backstage/plugin-events-node'; +import { EventSubscriber } from '@backstage/plugin-events-node'; import type { Logger } from 'winston'; import type { PermissionEvaluator } from '@backstage/plugin-permission-common'; import type { PluginDatabaseManager } from '@backstage/backend-common'; @@ -36,7 +37,7 @@ export class IncrementalCatalogBuilder { addIncrementalEntityProvider<TCursor, TContext>( provider: IncrementalEntityProvider<TCursor, TContext>, options: IncrementalEntityProviderOptions, - ): void; + ): EventSubscriber; // (undocumented) build(): Promise<{ incrementalAdminRouter: Router; @@ -50,6 +51,17 @@ export class IncrementalCatalogBuilder { // @public export interface IncrementalEntityProvider<TCursor, TContext> { around(burst: (context: TContext) => Promise<void>): Promise<void>; + eventHandler?: { + onEvent: (params: EventParams) => + | undefined + | { + added: DeferredEntity[]; + removed: { + entityRef: string; + }[]; + }; + supportsEventTopics: () => string[]; + }; getProviderName(): string; next( context: TContext, @@ -67,14 +79,6 @@ export interface IncrementalEntityProviderOptions { restLength: DurationObjectUnits; } -// @alpha -export const incrementalIngestionEntityProviderCatalogModule: (options: { - providers: { - provider: IncrementalEntityProvider<unknown, unknown>; - options: IncrementalEntityProviderOptions; - }[]; -}) => BackendFeature; - // @public (undocumented) export type PluginEnvironment = { logger: Logger; diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index bb2f3d1678..e3f6be3a88 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", "description": "An entity provider for streaming large asset sources into the catalog", - "version": "0.1.1", + "version": "0.2.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -36,12 +48,12 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@types/express": "^4.17.6", "@types/luxon": "^3.0.0", @@ -55,11 +67,12 @@ }, "devDependencies": { "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^" }, "files": [ - "alpha", "dist", "migrations/**/*.{js,d.ts}" ] diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/alpha.ts b/plugins/catalog-backend-module-incremental-ingestion/src/alpha.ts new file mode 100644 index 0000000000..c277a0a680 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './module'; diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.ts b/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.ts index e837f2b259..cf47942754 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.ts @@ -269,6 +269,17 @@ export class IncrementalIngestionDatabaseManager { }); } + /** + * This method is used to remove entity records from the ingestion_mark_entities + * table by their entity reference. + */ + async deleteEntityRecordsByRef(entities: { entityRef: string }[]) { + const refs = entities.map(e => e.entityRef); + await this.client.transaction(async tx => { + await tx('ingestion_mark_entities').delete().whereIn('ref', refs); + }); + } + /** * Creates a new ingestion record. * @param provider - string @@ -535,6 +546,21 @@ export class IncrementalIngestionDatabaseManager { }); } + /** + * Returns the first record from `ingestion_marks` for the supplied ingestionId. + * @param ingestionId - string + * @returns MarkRecord | undefined + */ + async getFirstMark(ingestionId: string) { + return await this.client.transaction(async tx => { + const mark = await tx<MarkRecord>('ingestion_marks') + .where('ingestion_id', ingestionId) + .orderBy('sequence', 'asc') + .first(); + return mark; + }); + } + async getAllMarks(ingestionId: string) { return await this.client.transaction(async tx => { const marks = await tx<MarkRecord>('ingestion_marks') diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts index e94bc9c53f..228c7a2505 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts @@ -21,8 +21,11 @@ import { performance } from 'perf_hooks'; import { Duration, DurationObjectUnits } from 'luxon'; import { v4 } from 'uuid'; import { stringifyError } from '@backstage/errors'; +import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; -export class IncrementalIngestionEngine implements IterationEngine { +export class IncrementalIngestionEngine + implements IterationEngine, EventSubscriber +{ private readonly restLength: Duration; private readonly backoff: DurationObjectUnits[]; @@ -329,4 +332,71 @@ export class IncrementalIngestionEngine implements IterationEngine { removed, }); } + + async onEvent(params: EventParams): Promise<void> { + const { topic } = params; + if (!this.supportsEventTopics().includes(topic)) { + return; + } + + const { logger, provider, connection } = this.options; + const providerName = provider.getProviderName(); + logger.debug(`incremental-engine: ${providerName} received ${topic} event`); + + if (!provider.eventHandler) { + return; + } + + const delta = provider.eventHandler.onEvent(params); + + if (delta) { + if (delta.added.length > 0) { + const ingestionRecord = await this.manager.getCurrentIngestionRecord( + providerName, + ); + + if (!ingestionRecord) { + logger.debug( + `incremental-engine: ${providerName} skipping delta addition because incremental ingestion is restarting.`, + ); + } else { + const mark = + ingestionRecord.status === 'resting' + ? await this.manager.getLastMark(ingestionRecord.id) + : await this.manager.getFirstMark(ingestionRecord.id); + + if (!mark) { + throw new Error( + `Cannot apply delta, page records are missing! Please re-run incremental ingestion for ${providerName}.`, + ); + } + await this.manager.createMarkEntities(mark.id, delta.added); + } + } + + if (delta.removed.length > 0) { + await this.manager.deleteEntityRecordsByRef(delta.removed); + } + + await connection.applyMutation({ + type: 'delta', + ...delta, + }); + logger.debug( + `incremental-engine: ${providerName} processed delta from '${topic}' event`, + ); + } else { + logger.warn( + `incremental-engine: Rejected delta from '${topic}' event - empty or invalid`, + ); + } + } + + supportsEventTopics(): string[] { + const { provider } = this.options; + const topics = provider.eventHandler + ? provider.eventHandler.supportsEventTopics() + : []; + return topics; + } } diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/index.ts b/plugins/catalog-backend-module-incremental-ingestion/src/index.ts index 976c91a1ce..ff1ee1faa1 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/index.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/index.ts @@ -20,7 +20,6 @@ * @packageDocumentation */ -export * from './module'; export * from './service'; export { type EntityIteratorResult, diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.ts index abe4cdb951..4738893c95 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.ts @@ -31,7 +31,7 @@ import { Duration } from 'luxon'; import { IncrementalIngestionDatabaseManager } from '../database/IncrementalIngestionDatabaseManager'; import { applyDatabaseMigrations } from '../database/migrations'; import { IncrementalIngestionEngine } from '../engine/IncrementalIngestionEngine'; -import { createIncrementalProviderRouter } from '../router/routes'; +import { IncrementalProviderRouter } from '../router/routes'; import { IncrementalEntityProvider, IncrementalEntityProviderOptions, @@ -75,10 +75,10 @@ export class WrapperProviders { } async adminRouter(): Promise<express.Router> { - return createIncrementalProviderRouter( + return await new IncrementalProviderRouter( new IncrementalIngestionDatabaseManager({ client: this.options.client }), loggerToWinstonLogger(this.options.logger), - ); + ).createRouter(); } private async startProvider( diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.test.ts index 06cbcb010e..dbadf573a4 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.test.ts @@ -18,7 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { coreServices } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { IncrementalEntityProvider } from '../types'; import { incrementalIngestionEntityProviderCatalogModule } from './incrementalIngestionEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.ts index b173b2d2ef..261768c142 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.ts @@ -18,11 +18,11 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { IncrementalEntityProvider, IncrementalEntityProviderOptions, -} from '../types'; +} from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; import { WrapperProviders } from './WrapperProviders'; /** diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/router/routes.ts b/plugins/catalog-backend-module-incremental-ingestion/src/router/routes.ts index 9f480e879e..4a89467a93 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/router/routes.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/router/routes.ts @@ -19,214 +19,235 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { IncrementalIngestionDatabaseManager } from '../database/IncrementalIngestionDatabaseManager'; -import { PROVIDER_BASE_PATH, PROVIDER_CLEANUP, PROVIDER_HEALTH } from './paths'; -export const createIncrementalProviderRouter = async ( - manager: IncrementalIngestionDatabaseManager, - logger: Logger, -) => { - const router = Router(); - router.use(express.json()); +export class IncrementalProviderRouter { + private manager: IncrementalIngestionDatabaseManager; + private logger: Logger; - // Get the overall health of all incremental providers - router.get(PROVIDER_HEALTH, async (_, res) => { - const records = await manager.healthcheck(); - const providers = records.map(record => record.provider_name); - const duplicates = [ - ...new Set(providers.filter((e, i, a) => a.indexOf(e) !== i)), - ]; + constructor(manager: IncrementalIngestionDatabaseManager, logger: Logger) { + this.manager = manager; + this.logger = logger; + } - if (duplicates.length > 0) { - res.json({ healthy: false, duplicateIngestions: duplicates }); - } else { - res.json({ healthy: true }); - } - }); + async createRouter() { + const router = Router(); + router.use(express.json()); - // Clean up and pause all providers - router.post(PROVIDER_CLEANUP, async (_, res) => { - const result = await manager.cleanupProviders(); - res.json(result); - }); + // Get the overall health of all incremental providers + router.get('/incremental/health', async (_, res) => { + const records = await this.manager.healthcheck(); + const providers = records.map(record => record.provider_name); + const duplicates = [ + ...new Set(providers.filter((e, i, a) => a.indexOf(e) !== i)), + ]; - // Get basic status of the provider - router.get(PROVIDER_BASE_PATH, async (req, res) => { - const { provider } = req.params; - const record = await manager.getCurrentIngestionRecord(provider); - if (record) { - res.json({ - success: true, - status: { - current_action: record.status, - next_action_at: new Date(record.next_action_at), - }, - last_error: record.last_error, - }); - } else { - const providers: string[] = await manager.listProviders(); - if (providers.includes(provider)) { + if (duplicates.length > 0) { + res.json({ healthy: false, duplicateIngestions: duplicates }); + } else { + res.json({ healthy: true }); + } + }); + + // Clean up and pause all providers + router.post('/incremental/cleanup', async (_, res) => { + const result = await this.manager.cleanupProviders(); + res.json(result); + }); + + // Get basic status of the provider + router.get('/incremental/providers/:provider', async (req, res) => { + const { provider } = req.params; + const record = await this.manager.getCurrentIngestionRecord(provider); + if (record) { res.json({ success: true, status: { - current_action: 'rest complete, waiting to start', + current_action: record.status, + next_action_at: new Date(record.next_action_at), }, + last_error: record.last_error, }); } else { - logger.error( - `${provider} - No ingestion record found in the database!`, - ); - res.status(404).json({ - success: false, - status: {}, - last_error: `Provider '${provider}' not found`, - }); + const providers: string[] = await this.manager.listProviders(); + if (providers.includes(provider)) { + res.json({ + success: true, + status: { + current_action: 'rest complete, waiting to start', + }, + }); + } else { + this.logger.error( + `${provider} - No ingestion record found in the database!`, + ); + res.status(404).json({ + success: false, + status: {}, + last_error: `Provider '${provider}' not found`, + }); + } } - } - }); - - // Trigger the provider's next action - router.post(`${PROVIDER_BASE_PATH}/trigger`, async (req, res) => { - const { provider } = req.params; - const record = await manager.getCurrentIngestionRecord(provider); - if (record) { - await manager.triggerNextProviderAction(provider); - res.json({ - success: true, - message: `${provider}: Next action triggered.`, - }); - } else { - const providers: string[] = await manager.listProviders(); - if (providers.includes(provider)) { - logger.debug(`${provider} - Ingestion record found`); - res.json({ - success: true, - message: 'Unable to trigger next action (provider is restarting)', - }); - } else { - res.status(404).json({ - success: false, - message: `Provider '${provider}' not found`, - }); - } - } - }); - - // Start a brand-new ingestion cycle for the provider. - // (Cancel's the current run if active, or marks it complete if resting) - router.post(`${PROVIDER_BASE_PATH}/start`, async (req, res) => { - const { provider } = req.params; - - const record = await manager.getCurrentIngestionRecord(provider); - if (record) { - const ingestionId = record.id; - if (record.status === 'resting') { - await manager.setProviderComplete(ingestionId); - } else { - await manager.setProviderCanceling(ingestionId); - } - res.json({ - success: true, - message: `${provider}: Next cycle triggered.`, - }); - } else { - const providers: string[] = await manager.listProviders(); - if (providers.includes(provider)) { - logger.debug(`${provider} - Ingestion record found`); - res.json({ - success: true, - message: 'Provider is already restarting', - }); - } else { - res.status(404).json({ - success: false, - message: `Provider '${provider}' not found`, - }); - } - } - }); - - // Stop the provider and pause it for 24 hours - router.post(`${PROVIDER_BASE_PATH}/cancel`, async (req, res) => { - const { provider } = req.params; - const record = await manager.getCurrentIngestionRecord(provider); - if (record) { - const next_action_at = new Date(); - next_action_at.setTime(next_action_at.getTime() + 24 * 60 * 60 * 1000); - await manager.updateByName(provider, { - next_action: 'nothing (done)', - ingestion_completed_at: new Date(), - next_action_at, - status: 'resting', - }); - res.json({ - success: true, - message: `${provider}: Current ingestion canceled.`, - }); - } else { - const providers: string[] = await manager.listProviders(); - if (providers.includes(provider)) { - logger.debug(`${provider} - Ingestion record found`); - res.json({ - success: true, - message: 'Provider is currently restarting, please wait.', - }); - } else { - res.status(404).json({ - success: false, - message: `Provider '${provider}' not found`, - }); - } - } - }); - - // Wipe out all ingestion records for the provider and pause for 24 hours - router.delete(PROVIDER_BASE_PATH, async (req, res) => { - const { provider } = req.params; - const result = await manager.purgeAndResetProvider(provider); - res.json(result); - }); - - // Get the ingestion marks for the current cycle - router.get(`${PROVIDER_BASE_PATH}/marks`, async (req, res) => { - const { provider } = req.params; - const record = await manager.getCurrentIngestionRecord(provider); - if (record) { - const id = record.id; - const records = await manager.getAllMarks(id); - res.json({ success: true, records }); - } else { - const providers: string[] = await manager.listProviders(); - if (providers.includes(provider)) { - logger.debug(`${provider} - Ingestion record found`); - res.json({ - success: true, - message: 'No records yet (provider is restarting)', - }); - } else { - logger.error( - `${provider} - No ingestion record found in the database!`, - ); - res.status(404).json({ - success: false, - status: {}, - last_error: `Provider '${provider}' not found`, - }); - } - } - }); - - router.delete(`${PROVIDER_BASE_PATH}/marks`, async (req, res) => { - const { provider } = req.params; - const deletions = await manager.clearFinishedIngestions(provider); - - res.json({ - success: true, - message: `Expired marks for provider '${provider}' removed.`, - deletions, }); - }); - router.use(errorHandler()); + // Trigger the provider's next action + router.post( + `/incremental/providers/:provider/trigger`, + async (req, res) => { + const { provider } = req.params; + const record = await this.manager.getCurrentIngestionRecord(provider); + if (record) { + await this.manager.triggerNextProviderAction(provider); + res.json({ + success: true, + message: `${provider}: Next action triggered.`, + }); + } else { + const providers: string[] = await this.manager.listProviders(); + if (providers.includes(provider)) { + this.logger.debug(`${provider} - Ingestion record found`); + res.json({ + success: true, + message: 'Unable to trigger next action (provider is restarting)', + }); + } else { + res.status(404).json({ + success: false, + message: `Provider '${provider}' not found`, + }); + } + } + }, + ); - return router; -}; + // Start a brand-new ingestion cycle for the provider. + // (Cancel's the current run if active, or marks it complete if resting) + router.post(`/incremental/providers/:provider/start`, async (req, res) => { + const { provider } = req.params; + + const record = await this.manager.getCurrentIngestionRecord(provider); + if (record) { + const ingestionId = record.id; + if (record.status === 'resting') { + await this.manager.setProviderComplete(ingestionId); + } else { + await this.manager.setProviderCanceling(ingestionId); + } + res.json({ + success: true, + message: `${provider}: Next cycle triggered.`, + }); + } else { + const providers: string[] = await this.manager.listProviders(); + if (providers.includes(provider)) { + this.logger.debug(`${provider} - Ingestion record found`); + res.json({ + success: true, + message: 'Provider is already restarting', + }); + } else { + res.status(404).json({ + success: false, + message: `Provider '${provider}' not found`, + }); + } + } + }); + + router.get(`/incremental/providers`, async (_req, res) => { + const providers = await this.manager.listProviders(); + + res.json({ + success: true, + providers, + }); + }); + + // Stop the provider and pause it for 24 hours + router.post(`/incremental/providers/:provider/cancel`, async (req, res) => { + const { provider } = req.params; + const record = await this.manager.getCurrentIngestionRecord(provider); + if (record) { + const next_action_at = new Date(); + next_action_at.setTime(next_action_at.getTime() + 24 * 60 * 60 * 1000); + await this.manager.updateByName(provider, { + next_action: 'nothing (done)', + ingestion_completed_at: new Date(), + next_action_at, + status: 'resting', + }); + res.json({ + success: true, + message: `${provider}: Current ingestion canceled.`, + }); + } else { + const providers: string[] = await this.manager.listProviders(); + if (providers.includes(provider)) { + this.logger.debug(`${provider} - Ingestion record found`); + res.json({ + success: true, + message: 'Provider is currently restarting, please wait.', + }); + } else { + res.status(404).json({ + success: false, + message: `Provider '${provider}' not found`, + }); + } + } + }); + + // Wipe out all ingestion records for the provider and pause for 24 hours + router.delete('/incremental/providers/:provider', async (req, res) => { + const { provider } = req.params; + const result = await this.manager.purgeAndResetProvider(provider); + res.json(result); + }); + + // Get the ingestion marks for the current cycle + router.get(`/incremental/providers/:provider/marks`, async (req, res) => { + const { provider } = req.params; + const record = await this.manager.getCurrentIngestionRecord(provider); + if (record) { + const id = record.id; + const records = await this.manager.getAllMarks(id); + res.json({ success: true, records }); + } else { + const providers: string[] = await this.manager.listProviders(); + if (providers.includes(provider)) { + this.logger.debug(`${provider} - Ingestion record found`); + res.json({ + success: true, + message: 'No records yet (provider is restarting)', + }); + } else { + this.logger.error( + `${provider} - No ingestion record found in the database!`, + ); + res.status(404).json({ + success: false, + status: {}, + last_error: `Provider '${provider}' not found`, + }); + } + } + }); + + router.delete( + `/incremental/providers/:provider/marks`, + async (req, res) => { + const { provider } = req.params; + const deletions = await this.manager.clearFinishedIngestions(provider); + + res.json({ + success: true, + message: `Expired marks for provider '${provider}' removed.`, + deletions, + }); + }, + ); + + router.use(errorHandler()); + + return router; + } +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/run.ts b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts index 08d95f4417..f1b6bd1529 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/run.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts @@ -14,27 +14,18 @@ * limitations under the License. */ -// eslint-disable-next-line import/no-extraneous-dependencies +// Think this file will probably go away once we move to backend system +// And restructure into the /dev folder +// eslint-disable-next-line @backstage/no-undeclared-imports +import { createBackend } from '@backstage/backend-defaults'; import { - databaseFactory, - discoveryFactory, - httpRouterFactory, - lifecycleFactory, - loggerFactory, - permissionsFactory, - rootLoggerFactory, - schedulerFactory, - tokenManagerFactory, - urlReaderFactory, -} from '@backstage/backend-app-api'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { startTestBackend } from '@backstage/backend-test-utils'; + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; import { ConfigReader } from '@backstage/config'; -import { catalogPlugin } from '@backstage/plugin-catalog-backend'; -import { - IncrementalEntityProvider, - incrementalIngestionEntityProviderCatalogModule, -} from '.'; +import { catalogPlugin } from '@backstage/plugin-catalog-backend/alpha'; +import { IncrementalEntityProvider } from '.'; +import { incrementalIngestionEntityProviderCatalogModule } from './alpha'; const provider: IncrementalEntityProvider<number, {}> = { getProviderName: () => 'test-provider', @@ -60,37 +51,33 @@ async function main() { }, }; - await startTestBackend({ + const backend = createBackend({ services: [ - [coreServices.config, new ConfigReader(config)], - databaseFactory(), - discoveryFactory(), - httpRouterFactory(), - lifecycleFactory(), - loggerFactory(), - permissionsFactory(), - rootLoggerFactory(), - schedulerFactory(), - tokenManagerFactory(), - urlReaderFactory(), - ], - extensionPoints: [], - features: [ - catalogPlugin(), - incrementalIngestionEntityProviderCatalogModule({ - providers: [ - { - provider: provider, - options: { - burstInterval: { seconds: 1 }, - burstLength: { seconds: 10 }, - restLength: { seconds: 10 }, - }, - }, - ], + createServiceFactory({ + service: coreServices.config, + deps: {}, + factory: () => new ConfigReader(config), }), ], }); + + backend.add(catalogPlugin()); + backend.add( + incrementalIngestionEntityProviderCatalogModule({ + providers: [ + { + provider: provider, + options: { + burstInterval: { seconds: 1 }, + burstLength: { seconds: 10 }, + restLength: { seconds: 10 }, + }, + }, + ], + }), + ); + + await backend.start(); } main().catch(error => { diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/service/IncrementalCatalogBuilder.ts b/plugins/catalog-backend-module-incremental-ingestion/src/service/IncrementalCatalogBuilder.ts index d6bc5549aa..c8458be09b 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/service/IncrementalCatalogBuilder.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/service/IncrementalCatalogBuilder.ts @@ -24,8 +24,9 @@ import { Knex } from 'knex'; import { IncrementalIngestionEngine } from '../engine/IncrementalIngestionEngine'; import { applyDatabaseMigrations } from '../database/migrations'; import { IncrementalIngestionDatabaseManager } from '../database/IncrementalIngestionDatabaseManager'; -import { createIncrementalProviderRouter } from '../router/routes'; +import { IncrementalProviderRouter } from '../router/routes'; import { Deferred } from '../util'; +import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; /** @public */ export class IncrementalCatalogBuilder { @@ -60,10 +61,10 @@ export class IncrementalCatalogBuilder { router: 'IncrementalProviderAdmin', }); - const incrementalAdminRouter = await createIncrementalProviderRouter( + const incrementalAdminRouter = await new IncrementalProviderRouter( this.manager, routerLogger, - ); + ).createRouter(); return { incrementalAdminRouter }; } @@ -71,13 +72,15 @@ export class IncrementalCatalogBuilder { addIncrementalEntityProvider<TCursor, TContext>( provider: IncrementalEntityProvider<TCursor, TContext>, options: IncrementalEntityProviderOptions, - ) { + ): EventSubscriber { const { burstInterval, burstLength, restLength } = options; const { logger: catalogLogger, scheduler } = this.env; const ready = this.ready; const manager = this.manager; + let engine: IncrementalIngestionEngine; + this.builder.addEntityProvider({ getProviderName: provider.getProviderName.bind(provider), async connect(connection) { @@ -87,7 +90,7 @@ export class IncrementalCatalogBuilder { logger.info(`Connecting`); - const engine = new IncrementalIngestionEngine({ + engine = new IncrementalIngestionEngine({ ...options, ready, manager, @@ -112,5 +115,12 @@ export class IncrementalCatalogBuilder { }); }, }); + + return { + onEvent: (params: EventParams) => engine.onEvent(params), + supportsEventTopics() { + return engine.supportsEventTopics(); + }, + }; } } diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/types.ts b/plugins/catalog-backend-module-incremental-ingestion/src/types.ts index 51a8738500..cd1e63b044 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/types.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/types.ts @@ -27,6 +27,7 @@ import type { DeferredEntity, EntityProviderConnection, } from '@backstage/plugin-catalog-backend'; +import { EventParams } from '@backstage/plugin-events-node'; import type { PermissionEvaluator } from '@backstage/plugin-permission-common'; import type { DurationObjectUnits } from 'luxon'; import type { Logger } from 'winston'; @@ -75,6 +76,37 @@ export interface IncrementalEntityProvider<TCursor, TContext> { * @param burst - a function which performs a series of iterations */ around(burst: (context: TContext) => Promise<void>): Promise<void>; + + /** + * If set, the IncrementalEntityProvider will receive and respond to + * events. + * + * This system acts as a wrapper for the Backstage events bus, and + * requires the events backend to function. It does not provide its + * own events backend. See {@link https://github.com/backstage/backstage/tree/master/plugins/events-backend}. + */ + eventHandler?: { + /** + * This method accepts an incoming event for the provider, and + * optionally maps the payload to an object containing a delta + * mutation. + * + * If a valid delta is returned by this method, it will be ingested + * automatically by the provider. + */ + onEvent: (params: EventParams) => + | undefined + | { + added: DeferredEntity[]; + removed: { entityRef: string }[]; + }; + + /** + * This method returns an array of topics for the IncrementalEntityProvider + * to respond to. + */ + supportsEventTopics: () => string[]; + }; } /** diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index c860f90323..e2c358d1a4 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.5.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.5.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/catalog-model@1.2.0 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + ## 0.5.7 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index e7911aa90f..dfca9332c0 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend module that helps integrate towards LDAP", - "version": "0.5.7", + "version": "0.5.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 2dbbfc0d09..c7c64da5a0 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,94 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.2-next.0 + +### Patch Changes + +- a4ebd51352: Revert using `baseUrl` for MS Graph provider as it breaks token retrieval +- 928a12a9b3: Internal refactor of `/alpha` exports. +- 2c234a8932: properly support custom graph api URL +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + +## 0.5.0 + +### Minor Changes + +- fb568e2683: Improve performance when loading users via group membership. + Users data is now loaded from a paged query, rather than having to make an extra call per user to load each user's profiles. + + Note, there are still additional per user calls made to load user avatars + +### Patch Changes + +- 4c86436fdf: Fix MS Graph provider to use target URL for fetching access token +- 0daa328c3a: Extract default transformers to their own file +- 28f9883440: Fixed a bug reading the `user.select` field expected from the `app-config.yaml` configuration +- c5b119ad9c: Increased default page size to 999 (from 100) to reduce the number of calls made to the Microsoft Graph API. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + +## 0.5.0-next.2 + +### Minor Changes + +- fb568e2683: Improve performance when loading users via group membership. + Users data is now loaded from a paged query, rather than having to make an extra call per user to load each user's profiles. + + Note, there are still additional per user calls made to load user avatars + +### Patch Changes + +- 28f9883440: Fixed a bug reading the `user.select` field expected from the `app-config.yaml` configuration +- c5b119ad9c: Increased default page size to 999 (from 100) to reduce the number of calls made to the Microsoft Graph API. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + +## 0.4.8-next.1 + +### Patch Changes + +- 4c86436fdf: Fix MS Graph provider to use target URL for fetching access token +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## 0.4.8-next.0 + +### Patch Changes + +- 0daa328c3a: Extract default transformers to their own file +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.4.6 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/alpha-api-report.md b/plugins/catalog-backend-module-msgraph/alpha-api-report.md new file mode 100644 index 0000000000..ed30b83bc8 --- /dev/null +++ b/plugins/catalog-backend-module-msgraph/alpha-api-report.md @@ -0,0 +1,24 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-msgraph" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { GroupTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; +import { OrganizationTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; +import { UserTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; + +// @alpha +export const microsoftGraphOrgEntityProviderCatalogModule: () => BackendFeature; + +// @alpha +export interface MicrosoftGraphOrgEntityProviderCatalogModuleOptions { + groupTransformer?: GroupTransformer | Record<string, GroupTransformer>; + organizationTransformer?: + | OrganizationTransformer + | Record<string, OrganizationTransformer>; + userTransformer?: UserTransformer | Record<string, UserTransformer>; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index a5abdc17c6..10a4f405c0 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; @@ -70,7 +69,11 @@ export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id'; export class MicrosoftGraphClient { constructor(baseUrl: string, tokenCredential: TokenCredential); static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient; - getGroupMembers(groupId: string): AsyncIterable<GroupMember>; + getGroupMembers( + groupId: string, + query?: ODataQuery, + queryMode?: 'basic' | 'advanced', + ): AsyncIterable<GroupMember>; // (undocumented) getGroupPhoto(groupId: string, sizeId?: string): Promise<string | undefined>; getGroupPhotoWithSizeLimit( @@ -81,6 +84,11 @@ export class MicrosoftGraphClient { query?: ODataQuery, queryMode?: 'basic' | 'advanced', ): AsyncIterable<MicrosoftGraph.Group>; + getGroupUserMembers( + groupId: string, + query?: ODataQuery, + queryMode?: 'basic' | 'advanced', + ): AsyncIterable<MicrosoftGraph.User>; getOrganization(tenantId: string): Promise<MicrosoftGraph.Organization>; // (undocumented) getUserPhoto(userId: string, sizeId?: string): Promise<string | undefined>; @@ -88,10 +96,6 @@ export class MicrosoftGraphClient { userId: string, maxSize: number, ): Promise<string | undefined>; - getUserProfile( - userId: string, - query?: ODataQuery, - ): Promise<MicrosoftGraph.User>; getUsers( query?: ODataQuery, queryMode?: 'basic' | 'advanced', @@ -134,18 +138,6 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { read(options?: { logger?: Logger }): Promise<void>; } -// @alpha -export const microsoftGraphOrgEntityProviderCatalogModule: () => BackendFeature; - -// @alpha -export interface MicrosoftGraphOrgEntityProviderCatalogModuleOptions { - groupTransformer?: GroupTransformer | Record<string, GroupTransformer>; - organizationTransformer?: - | OrganizationTransformer - | Record<string, OrganizationTransformer>; - userTransformer?: UserTransformer | Record<string, UserTransformer>; -} - // @public @deprecated export interface MicrosoftGraphOrgEntityProviderLegacyOptions { groupTransformer?: GroupTransformer; @@ -231,6 +223,7 @@ export type ODataQuery = { expand?: string; select?: string[]; count?: boolean; + top?: number; }; // @public diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index cd3ca706ac..29909d4c50 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", - "version": "0.4.6", + "version": "0.5.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -56,10 +68,9 @@ "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "luxon": "^3.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/catalog-backend-module-msgraph/src/alpha.ts b/plugins/catalog-backend-module-msgraph/src/alpha.ts new file mode 100644 index 0000000000..137808d881 --- /dev/null +++ b/plugins/catalog-backend-module-msgraph/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { microsoftGraphOrgEntityProviderCatalogModule } from './service/MicrosoftGraphOrgEntityProviderCatalogModule'; +export type { MicrosoftGraphOrgEntityProviderCatalogModuleOptions } from './service/MicrosoftGraphOrgEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-msgraph/src/index.ts b/plugins/catalog-backend-module-msgraph/src/index.ts index b80a0b3863..f98cb907ec 100644 --- a/plugins/catalog-backend-module-msgraph/src/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/index.ts @@ -22,5 +22,3 @@ export * from './microsoftGraph'; export * from './processors'; -export { microsoftGraphOrgEntityProviderCatalogModule } from './service/MicrosoftGraphOrgEntityProviderCatalogModule'; -export type { MicrosoftGraphOrgEntityProviderCatalogModuleOptions } from './service/MicrosoftGraphOrgEntityProviderCatalogModule'; 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 05693149d8..6d63537b54 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -53,7 +53,7 @@ describe('MicrosoftGraphClient', () => { expect(await response.json()).toEqual({ value: 'example' }); expect(tokenCredential.getToken).toHaveBeenCalledTimes(1); expect(tokenCredential.getToken).toHaveBeenCalledWith( - 'https://graph.microsoft.com/.default', + 'https://other.example.com/.default', ); }); @@ -70,7 +70,7 @@ describe('MicrosoftGraphClient', () => { expect(await response.json()).toEqual({ value: 'example' }); }); - it('should perform api request with filter, select and expand', async () => { + it('should perform api request with filter, select, expand and top', async () => { worker.use( rest.get('https://example.com/users', (req, res, ctx) => res(ctx.status(200), ctx.json({ queryString: req.url.search })), @@ -81,12 +81,13 @@ describe('MicrosoftGraphClient', () => { filter: 'test eq true', expand: 'children', select: ['id', 'children'], + top: 471, }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ queryString: - '?$filter=test%20eq%20true&$select=id,children&$expand=children', + '?$filter=test%20eq%20true&$select=id,children&$expand=children&$top=471', }); }); @@ -134,33 +135,6 @@ describe('MicrosoftGraphClient', () => { expect(values).toEqual(['first', 'second']); }); - it('should load user profile', async () => { - worker.use( - rest.get('https://example.com/users/user-id', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - surname: 'Example', - }), - ), - ), - ); - - const userProfile = await client.getUserProfile('user-id'); - - expect(userProfile).toEqual({ surname: 'Example' }); - }); - - it('should throw exception if load user profile fails', async () => { - worker.use( - rest.get('https://example.com/users/user-id', (_, res, ctx) => - res(ctx.status(404)), - ), - ); - - await expect(() => client.getUserProfile('user-id')).rejects.toThrow(); - }); - it('should load user profile photo with max size of 120', async () => { worker.use( rest.get('https://example.com/users/user-id/photos', (_, res, ctx) => @@ -329,6 +303,27 @@ describe('MicrosoftGraphClient', () => { ]); }); + it('should load user group members', async () => { + worker.use( + rest.get( + 'https://example.com/groups/group-id/members/microsoft.graph.user', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + value: [{ id: '12345' }, { id: '67890' }], + }), + ), + ), + ); + + const values = await collectAsyncIterable( + client.getGroupUserMembers('group-id'), + ); + + expect(values).toEqual([{ id: '12345' }, { id: '67890' }]); + }); + it('should load organization', async () => { worker.use( rest.get('https://example.com/organization/tentant-id', (_, res, ctx) => diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index 26da0cb64b..4acf7c726e 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -52,6 +52,10 @@ export type ODataQuery = { * Retrieves the total count of matching resources. */ count?: boolean; + /** + * Maximum number of records to receive in one batch. + */ + top?: number; }; /** @@ -188,6 +192,7 @@ export class MicrosoftGraphClient { $select: query?.select?.join(','), $expand: query?.expand, $count: query?.count, + $top: query?.top, }, { addQueryPrefix: true, @@ -213,8 +218,9 @@ export class MicrosoftGraphClient { headers?: Record<string, string>, ): Promise<Response> { // Make sure that we always have a valid access token (might be cached) + const urlObj = new URL(url); const token = await this.tokenCredential.getToken( - 'https://graph.microsoft.com/.default', + `${urlObj.protocol}//${urlObj.hostname}/.default`, ); if (!token) { @@ -229,28 +235,6 @@ export class MicrosoftGraphClient { }); } - /** - * Get {@link https://docs.microsoft.com/en-us/graph/api/resources/user | User} - * from Graph API - * - * @public - * @param userId - The unique identifier for the `User` resource - * @param query - OData Query {@link ODataQuery} - * - */ - async getUserProfile( - userId: string, - query?: ODataQuery, - ): Promise<MicrosoftGraph.User> { - const response = await this.requestApi(`users/${userId}`, query); - - if (response.status !== 200) { - await this.handleError('user profile', response); - } - - return await response.json(); - } - /** * Get {@link https://docs.microsoft.com/en-us/graph/api/resources/profilephoto | profilePhoto} * of `User` from Graph API with size limit @@ -343,8 +327,37 @@ export class MicrosoftGraphClient { * @param groupId - The unique identifier for the `Group` resource * */ - async *getGroupMembers(groupId: string): AsyncIterable<GroupMember> { - yield* this.requestCollection<GroupMember>(`groups/${groupId}/members`); + async *getGroupMembers( + groupId: string, + query?: ODataQuery, + queryMode?: 'basic' | 'advanced', + ): AsyncIterable<GroupMember> { + yield* this.requestCollection<GroupMember>( + `groups/${groupId}/members`, + query, + queryMode, + ); + } + + /** + * Get a collection of + * {@link https://docs.microsoft.com/en-us/graph/api/resources/user | User} + * belonging to a `Group` from Graph API and return as `AsyncIterable` + * @public + * @param groupId - The unique identifier for the `Group` resource + * @param query - OData Query {@link ODataQuery} + * @param queryMode - Mode to use while querying. Some features are only available at "advanced". + */ + async *getGroupUserMembers( + groupId: string, + query?: ODataQuery, + queryMode?: 'basic' | 'advanced', + ): AsyncIterable<MicrosoftGraph.User> { + yield* this.requestCollection<MicrosoftGraph.User>( + `groups/${groupId}/members/microsoft.graph.user/`, + query, + queryMode, + ); } /** 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 68c174665d..b02894dfd9 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -52,6 +52,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com/', userExpand: 'manager', userFilter: 'accountEnabled eq true', + userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', @@ -69,6 +70,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com/', userExpand: 'manager', userFilter: 'accountEnabled eq true', + userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', @@ -167,6 +169,7 @@ describe('readProviderConfigs', () => { user: { expand: 'manager', filter: 'accountEnabled eq true', + select: ['id', 'displayName', 'department'], }, group: { expand: 'member', @@ -196,6 +199,7 @@ describe('readProviderConfigs', () => { queryMode: 'advanced', userExpand: 'manager', userFilter: 'accountEnabled eq true', + userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], 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 506951ff10..cf4ec646a2 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -264,6 +264,7 @@ export function readProviderConfig( const userExpand = config.getOptionalString('user.expand'); const userFilter = config.getOptionalString('user.filter'); + const userSelect = config.getOptionalStringArray('user.select'); const groupExpand = config.getOptionalString('group.expand'); const groupFilter = config.getOptionalString('group.filter'); @@ -318,6 +319,7 @@ export function readProviderConfig( tenantId, userExpand, userFilter, + userSelect, groupExpand, groupFilter, groupSearch, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts new file mode 100644 index 0000000000..7a8c4ec463 --- /dev/null +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/defaultTransformers.ts @@ -0,0 +1,159 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + MICROSOFT_EMAIL_ANNOTATION, + MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, + MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, + MICROSOFT_GRAPH_USER_ID_ANNOTATION, +} from './constants'; +import { normalizeEntityName } from './helper'; +import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; + +/** + * The default implementation of the transformation from a graph organization + * entry to a Group entity. + * + * @public + */ +export async function defaultOrganizationTransformer( + organization: MicrosoftGraph.Organization, +): Promise<GroupEntity | undefined> { + if (!organization.id || !organization.displayName) { + return undefined; + } + + const name = normalizeEntityName(organization.displayName!); + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: name, + description: organization.displayName!, + annotations: { + [MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]: organization.id!, + }, + }, + spec: { + type: 'root', + profile: { + displayName: organization.displayName!, + }, + children: [], + }, + }; +} + +function extractGroupName(group: MicrosoftGraph.Group): string { + if (group.securityEnabled) { + return group.displayName as string; + } + return (group.mailNickname || group.displayName) as string; +} + +/** + * The default implementation of the transformation from a graph group entry to + * a Group entity. + * + * @public + */ +export async function defaultGroupTransformer( + group: MicrosoftGraph.Group, + groupPhoto?: string, +): Promise<GroupEntity | undefined> { + if (!group.id || !group.displayName) { + return undefined; + } + + const name = normalizeEntityName(extractGroupName(group)); + const entity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: name, + annotations: { + [MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id, + }, + }, + spec: { + type: 'team', + profile: {}, + children: [], + }, + }; + + if (group.description) { + entity.metadata.description = group.description; + } + if (group.displayName) { + entity.spec.profile!.displayName = group.displayName; + } + if (group.mail) { + entity.spec.profile!.email = group.mail; + } + if (groupPhoto) { + entity.spec.profile!.picture = groupPhoto; + } + + return entity; +} + +/** + * The default implementation of the transformation from a graph user entry to + * a User entity. + * + * @public + */ +export async function defaultUserTransformer( + user: MicrosoftGraph.User, + userPhoto?: string, +): Promise<UserEntity | undefined> { + if (!user.id || !user.displayName || !user.mail) { + return undefined; + } + + const name = normalizeEntityName(user.mail); + const entity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name, + annotations: { + [MICROSOFT_EMAIL_ANNOTATION]: user.mail!, + [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!, + }, + }, + spec: { + profile: { + displayName: user.displayName!, + email: user.mail!, + + // TODO: Additional fields? + // jobTitle: user.jobTitle || undefined, + // officeLocation: user.officeLocation || undefined, + // mobilePhone: user.mobilePhone || undefined, + }, + memberOf: [], + }, + }; + + if (userPhoto) { + entity.spec.profile!.picture = userPhoto; + } + + return entity; +} diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts index e9ab49f7d1..37dcffec22 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts @@ -29,8 +29,8 @@ export { defaultGroupTransformer, defaultOrganizationTransformer, defaultUserTransformer, - readMicrosoftGraphOrg, -} from './read'; +} from './defaultTransformers'; +export { readMicrosoftGraphOrg } from './read'; export type { GroupTransformer, OrganizationTransformer, 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 a537526a9e..b1dfc5a148 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -61,9 +61,9 @@ function group(data: Partial<GroupEntity>): GroupEntity { describe('read microsoft graph', () => { const client: jest.Mocked<MicrosoftGraphClient> = { getUsers: jest.fn(), - getUserProfile: jest.fn(), getGroups: jest.fn(), getGroupMembers: jest.fn(), + getGroupUserMembers: jest.fn(), getUserPhotoWithSizeLimit: jest.fn(), getGroupPhotoWithSizeLimit: jest.fn(), getOrganization: jest.fn(), @@ -71,16 +71,34 @@ describe('read microsoft graph', () => { afterEach(() => jest.resetAllMocks()); + async function* getExampleUsers() { + yield { + id: 'userid', + displayName: 'User Name', + mail: 'user.name@example.com', + }; + } + async function* getExampleGroups() { + yield { + id: 'groupid', + displayName: 'Group Name', + description: 'Group Description', + mail: 'group@example.com', + }; + } + async function* getExampleGroupMembers(): AsyncIterable<GroupMember> { + yield { + '@odata.type': '#microsoft.graph.group', + id: 'childgroupid', + }; + yield { + '@odata.type': '#microsoft.graph.user', + id: 'userid', + }; + } + describe('readMicrosoftGraphUsers', () => { it('should read users', async () => { - async function* getExampleUsers() { - yield { - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }; - } - client.getUsers.mockImplementation(getExampleUsers); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', @@ -115,6 +133,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toHaveBeenCalledWith( { filter: 'accountEnabled eq true', + top: 999, }, undefined, ); @@ -126,14 +145,6 @@ describe('read microsoft graph', () => { }); it('should read users with advanced query mode', async () => { - async function* getExampleUsers() { - yield { - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }; - } - client.getUsers.mockImplementation(getExampleUsers); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', @@ -169,6 +180,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toHaveBeenCalledWith( { filter: 'accountEnabled eq true', + top: 999, }, 'advanced', ); @@ -180,14 +192,6 @@ describe('read microsoft graph', () => { }); it('should read users with userExpand and custom transformer', async () => { - async function* getExampleUsers() { - yield { - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }; - } - client.getUsers.mockImplementation(getExampleUsers); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', @@ -219,6 +223,7 @@ describe('read microsoft graph', () => { { expand: 'manager', filter: 'accountEnabled eq true', + top: 999, }, undefined, ); @@ -232,34 +237,9 @@ describe('read microsoft graph', () => { describe('readMicrosoftGraphUsersInGroups', () => { it('should read users from Groups', async () => { - async function* getExampleGroups() { - yield { - id: 'groupid', - displayName: 'Group Name', - description: 'Group Description', - mail: 'group@example.com', - }; - } - - async function* getExampleGroupMembers(): AsyncIterable<GroupMember> { - yield { - '@odata.type': '#microsoft.graph.group', - id: 'childgroupid', - }; - yield { - '@odata.type': '#microsoft.graph.user', - id: 'userid', - }; - } - client.getGroups.mockImplementation(getExampleGroups); - client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getGroupUserMembers.mockImplementation(getExampleUsers); - client.getUserProfile.mockResolvedValue({ - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -293,16 +273,19 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq true', + select: ['id', 'displayName'], + top: 999, }, undefined, ); - expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); - expect(client.getUserProfile).toHaveBeenCalledTimes(1); - expect(client.getUserProfile).toHaveBeenCalledWith('userid', { - expand: undefined, - }); + expect(client.getGroupUserMembers).toHaveBeenCalledTimes(1); + expect(client.getGroupUserMembers).toHaveBeenCalledWith( + 'groupid', + { top: 999 }, + undefined, + ); + expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( 'userid', @@ -311,34 +294,9 @@ describe('read microsoft graph', () => { }); it('should read users from Groups with advanced query mode', async () => { - async function* getExampleGroups() { - yield { - id: 'groupid', - displayName: 'Group Name', - description: 'Group Description', - mail: 'group@example.com', - }; - } - - async function* getExampleGroupMembers(): AsyncIterable<GroupMember> { - yield { - '@odata.type': '#microsoft.graph.group', - id: 'childgroupid', - }; - yield { - '@odata.type': '#microsoft.graph.user', - id: 'userid', - }; - } - client.getGroups.mockImplementation(getExampleGroups); - client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getGroupUserMembers.mockImplementation(getExampleUsers); - client.getUserProfile.mockResolvedValue({ - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -373,16 +331,19 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq true', + select: ['id', 'displayName'], + top: 999, }, 'advanced', ); - expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); - expect(client.getUserProfile).toHaveBeenCalledTimes(1); - expect(client.getUserProfile).toHaveBeenCalledWith('userid', { - expand: undefined, - }); + expect(client.getGroupUserMembers).toHaveBeenCalledTimes(1); + expect(client.getGroupUserMembers).toHaveBeenCalledWith( + 'groupid', + { top: 999 }, + 'advanced', + ); + expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( 'userid', @@ -391,34 +352,9 @@ describe('read microsoft graph', () => { }); it('should read users with userExpand, groupExpand and custom transformer', async () => { - async function* getExampleGroups() { - yield { - id: 'groupid', - displayName: 'Group Name', - description: 'Group Description', - mail: 'group@example.com', - }; - } - - async function* getExampleGroupMembers(): AsyncIterable<GroupMember> { - yield { - '@odata.type': '#microsoft.graph.group', - id: 'childgroupid', - }; - yield { - '@odata.type': '#microsoft.graph.user', - id: 'userid', - }; - } - client.getGroups.mockImplementation(getExampleGroups); - client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getGroupUserMembers.mockImplementation(getExampleUsers); - client.getUserProfile.mockResolvedValue({ - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -450,16 +386,22 @@ describe('read microsoft graph', () => { { expand: 'member', filter: 'securityEnabled eq true', + select: ['id', 'displayName'], + top: 999, + }, + undefined, + ); + + expect(client.getGroupUserMembers).toHaveBeenCalledTimes(1); + expect(client.getGroupUserMembers).toHaveBeenCalledWith( + 'groupid', + { + expand: 'manager', + top: 999, }, undefined, ); - expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); - expect(client.getUserProfile).toHaveBeenCalledTimes(1); - expect(client.getUserProfile).toHaveBeenCalledWith('userid', { - expand: 'manager', - }); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( 'userid', @@ -524,26 +466,6 @@ describe('read microsoft graph', () => { describe('readMicrosoftGraphGroups', () => { it('should read groups', async () => { - async function* getExampleGroups() { - yield { - id: 'groupid', - displayName: 'Group Name', - description: 'Group Description', - mail: 'group@example.com', - }; - } - - async function* getExampleGroupMembers(): AsyncIterable<GroupMember> { - yield { - '@odata.type': '#microsoft.graph.group', - id: 'childgroupid', - }; - yield { - '@odata.type': '#microsoft.graph.user', - id: 'userid', - }; - } - client.getGroups.mockImplementation(getExampleGroups); client.getGroupMembers.mockImplementation(getExampleGroupMembers); client.getOrganization.mockResolvedValue({ @@ -607,11 +529,14 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -619,26 +544,6 @@ describe('read microsoft graph', () => { }); it('should read groups with advanced query mode', async () => { - async function* getExampleGroups() { - yield { - id: 'groupid', - displayName: 'Group Name', - description: 'Group Description', - mail: 'group@example.com', - }; - } - - async function* getExampleGroupMembers(): AsyncIterable<GroupMember> { - yield { - '@odata.type': '#microsoft.graph.group', - id: 'childgroupid', - }; - yield { - '@odata.type': '#microsoft.graph.user', - id: 'userid', - }; - } - client.getGroups.mockImplementation(getExampleGroups); client.getGroupMembers.mockImplementation(getExampleGroupMembers); client.getOrganization.mockResolvedValue({ @@ -703,11 +608,14 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, 'advanced', ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -715,26 +623,6 @@ describe('read microsoft graph', () => { }); it('should read groups with groupExpand', async () => { - async function* getExampleGroups() { - yield { - id: 'groupid', - displayName: 'Group Name', - description: 'Group Description', - mail: 'group@example.com', - }; - } - - async function* getExampleGroupMembers(): AsyncIterable<GroupMember> { - yield { - '@odata.type': '#microsoft.graph.group', - id: 'childgroupid', - }; - yield { - '@odata.type': '#microsoft.graph.user', - id: 'userid', - }; - } - client.getGroups.mockImplementation(getExampleGroups); client.getGroupMembers.mockImplementation(getExampleGroupMembers); client.getOrganization.mockResolvedValue({ @@ -800,11 +688,14 @@ describe('read microsoft graph', () => { { expand: 'member', filter: 'securityEnabled eq false', + top: 999, }, undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -812,29 +703,6 @@ describe('read microsoft graph', () => { }); it('should read security groups', async () => { - async function* getExampleGroups() { - yield { - id: 'groupid', - displayName: 'Group Name', - description: 'Group Description', - mail: 'group@example.com', - mailNickname: 'df546d53-4f5f-4462-b371-d4a855787047', - mailEnabled: false, - securityEnabled: true, - }; - } - - async function* getExampleGroupMembers(): AsyncIterable<GroupMember> { - yield { - '@odata.type': '#microsoft.graph.group', - id: 'childgroupid', - }; - yield { - '@odata.type': '#microsoft.graph.user', - id: 'userid', - }; - } - client.getGroups.mockImplementation(getExampleGroups); client.getGroupMembers.mockImplementation(getExampleGroupMembers); client.getOrganization.mockResolvedValue({ @@ -893,11 +761,14 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq true', + top: 999, }, undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); }); }); @@ -994,53 +865,21 @@ describe('read microsoft graph', () => { }); describe('readMicrosoftGraphOrg', () => { - async function* getExampleUsers() { - yield { - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }; - } - async function* getExampleUsersEmail() { yield { mail: 'user.name@example.com', }; } - async function getExampleUserProfile(userId: string) { + function getExampleOrg() { return { - id: userId, - displayName: 'User Name', - mail: 'user.name@example.com', - }; - } - - async function* getExampleGroups() { - yield { - id: 'groupid', - displayName: 'Group Name', - description: 'Group Description', - mail: 'group@example.com', - }; - } - - async function* getExampleGroupMembers(): AsyncIterable<GroupMember> { - yield { - '@odata.type': '#microsoft.graph.group', - id: 'childgroupid', - }; - yield { - '@odata.type': '#microsoft.graph.user', - id: 'userid', + id: 'tenantid', + displayName: 'Organization Name', }; } it('should read all users if no filter provided', async () => { - client.getOrganization.mockResolvedValue({ - id: 'tenantid', - displayName: 'Organization Name', - }); + client.getOrganization.mockResolvedValue(getExampleOrg()); client.getUsers.mockImplementation(getExampleUsers); client.getUserPhotoWithSizeLimit.mockResolvedValue( @@ -1062,6 +901,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toHaveBeenCalledWith( { filter: undefined, + top: 999, }, undefined, ); @@ -1069,16 +909,14 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, undefined, ); }); it('should read users using userExpand and userFilter', async () => { - client.getOrganization.mockResolvedValue({ - id: 'tenantid', - displayName: 'Organization Name', - }); + client.getOrganization.mockResolvedValue(getExampleOrg()); client.getUsers.mockImplementation(getExampleUsers); client.getUserPhotoWithSizeLimit.mockResolvedValue( @@ -1103,6 +941,7 @@ describe('read microsoft graph', () => { { expand: 'manager', filter: 'accountEnabled eq true', + top: 999, }, undefined, ); @@ -1110,6 +949,7 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, undefined, ); @@ -1138,6 +978,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toHaveBeenCalledWith( { select: ['mail'], + top: 999, }, undefined, ); @@ -1150,13 +991,13 @@ describe('read microsoft graph', () => { }); client.getUsers.mockImplementation(getExampleUsers); - client.getUserProfile.mockImplementation(getExampleUserProfile); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); client.getGroups.mockImplementation(getExampleGroups); client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getGroupUserMembers.mockImplementation(getExampleUsers); client.getGroupPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -1172,16 +1013,18 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'name eq backstage-group', + select: ['id', 'displayName'], + top: 999, }, undefined, ); expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, undefined, ); - expect(client.getUserProfile).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 0ed411bfea..003448a3e8 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -19,69 +19,28 @@ import { stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; import { MicrosoftGraphClient } from './client'; import { - MICROSOFT_EMAIL_ANNOTATION, MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, MICROSOFT_GRAPH_USER_ID_ANNOTATION, } from './constants'; -import { normalizeEntityName } from './helper'; import { buildMemberOf, buildOrgHierarchy } from './org'; import { GroupTransformer, OrganizationTransformer, UserTransformer, } from './types'; +import { + defaultGroupTransformer, + defaultOrganizationTransformer, + defaultUserTransformer, +} from './defaultTransformers'; +import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; -/** - * The default implementation of the transformation from a graph user entry to - * a User entity. - * - * @public - */ -export async function defaultUserTransformer( - user: MicrosoftGraph.User, - userPhoto?: string, -): Promise<UserEntity | undefined> { - if (!user.id || !user.displayName || !user.mail) { - return undefined; - } - - const name = normalizeEntityName(user.mail); - const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name, - annotations: { - [MICROSOFT_EMAIL_ANNOTATION]: user.mail!, - [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!, - }, - }, - spec: { - profile: { - displayName: user.displayName!, - email: user.mail!, - - // TODO: Additional fields? - // jobTitle: user.jobTitle || undefined, - // officeLocation: user.officeLocation || undefined, - // mobilePhone: user.mobilePhone || undefined, - }, - memberOf: [], - }, - }; - - if (userPhoto) { - entity.spec.profile!.picture = userPhoto; - } - - return entity; -} +const PAGE_SIZE = 999; export async function readMicrosoftGraphUsers( client: MicrosoftGraphClient, @@ -96,50 +55,24 @@ export async function readMicrosoftGraphUsers( ): Promise<{ users: UserEntity[]; // With all relations empty }> { - const users: UserEntity[] = []; - const limiter = limiterFactory(10); - - const transformer = options.transformer ?? defaultUserTransformer; - const promises: Promise<void>[] = []; - - for await (const user of client.getUsers( + const users = client.getUsers( { filter: options.userFilter, expand: options.userExpand, select: options.userSelect, + top: PAGE_SIZE, }, options.queryMode, - )) { - // Process all users in parallel, otherwise it can take quite some time - promises.push( - limiter(async () => { - let userPhoto; - try { - userPhoto = await client.getUserPhotoWithSizeLimit( - user.id!, - // We are limiting the photo size, as users with full resolution photos - // can make the Backstage API slow - 120, - ); - } catch (e) { - options.logger.warn(`Unable to load photo for ${user.id}, ${e}`); - } + ); - const entity = await transformer(user, userPhoto); - - if (!entity) { - return; - } - - users.push(entity); - }), - ); - } - - // Wait for all users and photos to be downloaded - await Promise.all(promises); - - return { users }; + return { + users: await transformUsers( + client, + users, + options.logger, + options.transformer, + ), + }; } export async function readMicrosoftGraphUsersInGroups( @@ -157,36 +90,41 @@ export async function readMicrosoftGraphUsersInGroups( ): Promise<{ users: UserEntity[]; // With all relations empty }> { - const users: UserEntity[] = []; - const limiter = limiterFactory(10); - const transformer = options.transformer ?? defaultUserTransformer; const userGroupMemberPromises: Promise<void>[] = []; - const userPromises: Promise<void>[] = []; - - const groupMemberUsers: Set<string> = new Set(); + const userGroupMembers = new Map<string, MicrosoftGraph.User>(); for await (const group of client.getGroups( { expand: options.groupExpand, search: options.userGroupMemberSearch, filter: options.userGroupMemberFilter, + select: ['id', 'displayName'], + top: PAGE_SIZE, }, options.queryMode, )) { // Process all groups in parallel, otherwise it can take quite some time userGroupMemberPromises.push( limiter(async () => { - for await (const member of client.getGroupMembers(group.id!)) { - if (!member.id) { - continue; - } - - if (member['@odata.type'] === '#microsoft.graph.user') { - groupMemberUsers.add(member.id); - } + let groupMemberCount = 0; + for await (const user of client.getGroupUserMembers( + group.id!, + { + expand: options.userExpand, + top: PAGE_SIZE, + }, + options.queryMode, + )) { + userGroupMembers.set(user.id!, user); + groupMemberCount++; } + options.logger.debug('Read users from group', { + groupId: group.id, + groupName: group.displayName, + memberCount: groupMemberCount, + }); }), ); } @@ -194,80 +132,18 @@ export async function readMicrosoftGraphUsersInGroups( // Wait for all group members await Promise.all(userGroupMemberPromises); - options.logger.info(`groupMemberUsers ${groupMemberUsers.size}`); - for (const userId of groupMemberUsers) { - // Process all users in parallel, otherwise it can take quite some time - userPromises.push( - limiter(async () => { - let user; - let userPhoto; - try { - user = await client.getUserProfile(userId, { - expand: options.userExpand, - }); - } catch (e) { - options.logger.warn(`Unable to load user for ${userId}, ${e}`); - } - if (user) { - try { - userPhoto = await client.getUserPhotoWithSizeLimit( - user.id!, - // We are limiting the photo size, as users with full resolution photos - // can make the Backstage API slow - 120, - ); - } catch (e) { - options.logger.warn(`Unable to load userphoto for ${userId}, ${e}`); - } + options.logger.info('Read users from group membership', { + groupCount: userGroupMemberPromises.length, + userCount: userGroupMembers.size, + }); - const entity = await transformer(user, userPhoto); - - if (!entity) { - return; - } - users.push(entity); - } - }), - ); - } - - // Wait for all users and photos to be downloaded - await Promise.all(userPromises); - - return { users }; -} - -/** - * The default implementation of the transformation from a graph organization - * entry to a Group entity. - * - * @public - */ -export async function defaultOrganizationTransformer( - organization: MicrosoftGraph.Organization, -): Promise<GroupEntity | undefined> { - if (!organization.id || !organization.displayName) { - return undefined; - } - - const name = normalizeEntityName(organization.displayName!); return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: name, - description: organization.displayName!, - annotations: { - [MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]: organization.id!, - }, - }, - spec: { - type: 'root', - profile: { - displayName: organization.displayName!, - }, - children: [], - }, + users: await transformUsers( + client, + userGroupMembers.values(), + options.logger, + options.transformer, + ), }; } @@ -286,60 +162,6 @@ export async function readMicrosoftGraphOrganization( return { rootGroup }; } -function extractGroupName(group: MicrosoftGraph.Group): string { - if (group.securityEnabled) { - return group.displayName as string; - } - return (group.mailNickname || group.displayName) as string; -} - -/** - * The default implementation of the transformation from a graph group entry to - * a Group entity. - * - * @public - */ -export async function defaultGroupTransformer( - group: MicrosoftGraph.Group, - groupPhoto?: string, -): Promise<GroupEntity | undefined> { - if (!group.id || !group.displayName) { - return undefined; - } - - const name = normalizeEntityName(extractGroupName(group)); - const entity: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: name, - annotations: { - [MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id, - }, - }, - spec: { - type: 'team', - profile: {}, - children: [], - }, - }; - - if (group.description) { - entity.metadata.description = group.description; - } - if (group.displayName) { - entity.spec.profile!.displayName = group.displayName; - } - if (group.mail) { - entity.spec.profile!.email = group.mail; - } - if (groupPhoto) { - entity.spec.profile!.picture = groupPhoto; - } - - return entity; -} - export async function readMicrosoftGraphGroups( client: MicrosoftGraphClient, tenantId: string, @@ -380,6 +202,7 @@ export async function readMicrosoftGraphGroups( search: options?.groupSearch, filter: options?.groupFilter, select: options?.groupSelect, + top: PAGE_SIZE, }, options?.queryMode, )) { @@ -401,7 +224,9 @@ export async function readMicrosoftGraphGroups( return; } - for await (const member of client.getGroupMembers(group.id!)) { + for await (const member of client.getGroupMembers(group.id!, { + top: PAGE_SIZE, + })) { if (!member.id) { continue; } @@ -593,6 +418,56 @@ export async function readMicrosoftGraphOrg( return { users, groups }; } +async function transformUsers( + client: MicrosoftGraphClient, + users: Iterable<MicrosoftGraph.User> | AsyncIterable<MicrosoftGraph.User>, + logger: Logger, + transformer?: UserTransformer, +) { + const limiter = limiterFactory(10); + + const resolvedTransformer = transformer ?? defaultUserTransformer; + const promises: Promise<void>[] = []; + const entities: UserEntity[] = []; + + // Process all users in parallel, otherwise it can take quite some time + for await (const user of users) { + promises.push( + limiter(async () => { + let userPhoto; + try { + userPhoto = await client.getUserPhotoWithSizeLimit( + user.id!, + // We are limiting the photo size, as users with full resolution photos + // can make the Backstage API slow + 120, + ); + } catch (e) { + logger.warn(`Unable to load user photo for`, { + user: user.id, + error: e, + }); + } + + const entity = await resolvedTransformer(user, userPhoto); + + if (entity) { + entities.push(entity); + } + }), + ); + } + + // Wait for all users and photos to be downloaded + await Promise.all(promises); + + logger.debug('Finished transforming users', { + microsoftUserCount: promises.length, + backstageUserCount: entities.length, + }); + return entities; +} + function ensureItem( target: Map<string, Set<string>>, key: string, diff --git a/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.test.ts index 27ae5b9dd8..3763553162 100644 --- a/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/backend-tasks'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; import { microsoftGraphOrgEntityProviderCatalogModule } from './MicrosoftGraphOrgEntityProviderCatalogModule'; import { MicrosoftGraphOrgEntityProvider } from '../processors'; diff --git a/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.ts index fc63867a23..11be567045 100644 --- a/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.ts @@ -19,12 +19,12 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { GroupTransformer, OrganizationTransformer, UserTransformer, -} from '../microsoftGraph'; +} from '@backstage/plugin-catalog-backend-module-msgraph'; import { MicrosoftGraphOrgEntityProvider } from '../processors'; /** diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 522202cd43..a1d43e6954 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-node@1.3.3-next.1 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + ## 0.1.6 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index d39d3aa304..3407eeb0e6 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", - "version": "0.1.6", + "version": "0.1.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 904cc379a6..0b55db0239 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,121 @@ # @backstage/plugin-catalog-backend +## 1.8.0-next.0 + +### Minor Changes + +- 7f4ea3d360: Add /entities/by-query endpoint returning paginated entities. + + The endpoint supports cursor base pagination and server side sorting of the entities + +### Patch Changes + +- ac8929f2f3: Fix export of `defaultCatalogCollatorEntityTransformer`. +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.1 + +## 1.7.2 + +### Patch Changes + +- 071354eb7d: Add additional validation as security precations for output entities. +- b977c2e69f: Minor improvements to the descriptions provided with permission rules schemas +- 2380506364: The process of adding or modifying fields in the software-catalog search index has been simplified. For more details, see [how to customize fields in the Software Catalog index](https://backstage.io/docs/features/search/how-to-guides#how-to-customize-fields-in-the-software-catalog-index). +- 9573651919: The previous migration that adds the `search.original_value` column may leave some of the entities not updated. Add a migration script to trigger a reprocessing of the entities. +- 9f71a2fd20: Location rule target patterns now also match hidden files, i.e. path components with a leading dot. +- e716946103: Updated usage of the lifecycle service. +- 1aec041c34: Fixed an issue where entities sometimes were not properly deleted during a full mutation. +- 0ff03319be: Updated usage of `createBackendPlugin`. +- fc73f6aae5: Switched the order of reprocessing statements retroactively in migrations. This only improves the experience for those who at a later time perform a large upgrade of an old Backstage installation. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + - @backstage/plugin-scaffolder-common@1.2.5 + - @backstage/plugin-search-common@1.2.1 + +## 1.7.2-next.2 + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + - @backstage/plugin-search-common@1.2.1 + +## 1.7.2-next.1 + +### Patch Changes + +- 2380506364: The process of adding or modifying fields in the software-catalog search index has been simplified. For more details, see [how to customize fields in the Software Catalog index](https://backstage.io/docs/features/search/how-to-guides#how-to-customize-fields-in-the-software-catalog-index). +- 9573651919: The previous migration that adds the `search.original_value` column may leave some of the entities not updated. Add a migration script to trigger a reprocessing of the entities. +- fc73f6aae5: Switched the order of reprocessing statements retroactively in migrations. This only improves the experience for those who at a later time perform a large upgrade of an old Backstage installation. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/plugin-search-common@1.2.1 + +## 1.7.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 1.7.0 ### Minor Changes diff --git a/plugins/catalog-backend/alpha-api-report.md b/plugins/catalog-backend/alpha-api-report.md new file mode 100644 index 0000000000..df2fa3d2e9 --- /dev/null +++ b/plugins/catalog-backend/alpha-api-report.md @@ -0,0 +1,156 @@ +## API Report File for "@backstage/plugin-catalog-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { ConditionalPolicyDecision } from '@backstage/plugin-permission-common'; +import { Conditions } from '@backstage/plugin-permission-node'; +import { Entity } from '@backstage/catalog-model'; +import { PermissionCondition } from '@backstage/plugin-permission-common'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { PermissionRule } from '@backstage/plugin-permission-node'; +import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { ResourcePermission } from '@backstage/plugin-permission-common'; + +// @alpha +export const catalogConditions: Conditions<{ + hasAnnotation: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + value?: string | undefined; + annotation: string; + } + >; + hasLabel: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + label: string; + } + >; + hasMetadata: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + value?: string | undefined; + key: string; + } + >; + hasSpec: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + value?: string | undefined; + key: string; + } + >; + isEntityKind: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + kinds: string[]; + } + >; + isEntityOwner: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + claims: string[]; + } + >; +}>; + +// @alpha +export type CatalogPermissionRule< + TParams extends PermissionRuleParams = PermissionRuleParams, +> = PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>; + +// @alpha +export const catalogPlugin: () => BackendFeature; + +// @alpha +export const createCatalogConditionalDecision: ( + permission: ResourcePermission<'catalog-entity'>, + conditions: PermissionCriteria< + PermissionCondition<'catalog-entity', PermissionRuleParams> + >, +) => ConditionalPolicyDecision; + +// @alpha +export const createCatalogPermissionRule: < + TParams extends PermissionRuleParams = undefined, +>( + rule: PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>, +) => PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>; + +// @public +export type EntitiesSearchFilter = { + key: string; + values?: string[]; +}; + +// @alpha +export const permissionRules: { + hasAnnotation: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + value?: string | undefined; + annotation: string; + } + >; + hasLabel: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + label: string; + } + >; + hasMetadata: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + value?: string | undefined; + key: string; + } + >; + hasSpec: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + value?: string | undefined; + key: string; + } + >; + isEntityKind: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + kinds: string[]; + } + >; + isEntityOwner: PermissionRule< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + { + claims: string[]; + } + >; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 6ff9ac4b1f..00c13a31ea 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -10,7 +10,6 @@ import { AnalyzeLocationExistingEntity as AnalyzeLocationExistingEntity_2 } from import { AnalyzeLocationGenerateEntity as AnalyzeLocationGenerateEntity_2 } from '@backstage/plugin-catalog-common'; import { AnalyzeLocationRequest as AnalyzeLocationRequest_2 } from '@backstage/plugin-catalog-common'; import { AnalyzeLocationResponse as AnalyzeLocationResponse_2 } from '@backstage/plugin-catalog-common'; -import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; @@ -23,8 +22,6 @@ import { CatalogProcessorParser } from '@backstage/plugin-catalog-node'; import { CatalogProcessorRefreshKeysResult } from '@backstage/plugin-catalog-node'; import { CatalogProcessorRelationResult } from '@backstage/plugin-catalog-node'; import { CatalogProcessorResult } from '@backstage/plugin-catalog-node'; -import { ConditionalPolicyDecision } from '@backstage/plugin-permission-common'; -import { Conditions } from '@backstage/plugin-permission-node'; import { Config } from '@backstage/config'; import { DeferredEntity } from '@backstage/plugin-catalog-node'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; @@ -41,8 +38,6 @@ import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; -import { PermissionCondition } from '@backstage/plugin-permission-common'; -import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; @@ -50,7 +45,6 @@ import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { processingResult } from '@backstage/plugin-catalog-node'; import { Readable } from 'stream'; -import { ResourcePermission } from '@backstage/plugin-permission-common'; import { Router } from 'express'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TokenManager } from '@backstage/backend-common'; @@ -128,10 +122,9 @@ export class CatalogBuilder { addLocationAnalyzers( ...analyzers: Array<ScmLocationAnalyzer | Array<ScmLocationAnalyzer>> ): CatalogBuilder; - // @alpha addPermissionRules( ...permissionRules: Array< - CatalogPermissionRule | Array<CatalogPermissionRule> + CatalogPermissionRuleInput | Array<CatalogPermissionRuleInput> > ): this; addProcessor( @@ -167,60 +160,10 @@ export class CatalogBuilder { useLegacySingleProcessorValidation(): this; } -// @alpha -export const catalogConditions: Conditions<{ - hasAnnotation: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - value?: string | undefined; - annotation: string; - } - >; - hasLabel: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - label: string; - } - >; - hasMetadata: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - value?: string | undefined; - key: string; - } - >; - hasSpec: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - value?: string | undefined; - key: string; - } - >; - isEntityKind: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - kinds: string[]; - } - >; - isEntityOwner: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - claims: string[]; - } - >; -}>; +// @public (undocumented) +export type CatalogCollatorEntityTransformer = ( + entity: Entity, +) => Omit<CatalogEntityDocument, 'location' | 'authorization'>; // @public (undocumented) export type CatalogEnvironment = { @@ -231,14 +174,11 @@ export type CatalogEnvironment = { permissions: PermissionEvaluator | PermissionAuthorizer; }; -// @alpha -export type CatalogPermissionRule< +// @public +export type CatalogPermissionRuleInput< TParams extends PermissionRuleParams = PermissionRuleParams, > = PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>; -// @alpha -export const catalogPlugin: () => BackendFeature; - // @public export interface CatalogProcessingEngine { // (undocumented) @@ -288,21 +228,6 @@ export class CodeOwnersProcessor implements CatalogProcessor { preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>; } -// @alpha -export const createCatalogConditionalDecision: ( - permission: ResourcePermission<'catalog-entity'>, - conditions: PermissionCriteria< - PermissionCondition<'catalog-entity', PermissionRuleParams> - >, -) => ConditionalPolicyDecision; - -// @alpha -export const createCatalogPermissionRule: < - TParams extends PermissionRuleParams = undefined, ->( - rule: PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>, -) => PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>; - // @public export function createRandomProcessingInterval(options: { minSeconds: number; @@ -350,6 +275,9 @@ export class DefaultCatalogCollator { readonly visibilityPermission: Permission; } +// @public (undocumented) +export const defaultCatalogCollatorEntityTransformer: CatalogCollatorEntityTransformer; + // @public (undocumented) export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // (undocumented) @@ -360,7 +288,7 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // (undocumented) getCollator(): Promise<Readable>; // (undocumented) - readonly type: string; + readonly type = 'software-catalog'; // (undocumented) readonly visibilityPermission: Permission; } @@ -373,6 +301,7 @@ export type DefaultCatalogCollatorFactoryOptions = { filter?: GetEntitiesRequest['filter']; batchSize?: number; catalogClient?: CatalogApi; + entityTransformer?: CatalogCollatorEntityTransformer; }; export { DeferredEntity }; @@ -457,61 +386,6 @@ export function parseEntityYaml( location: LocationSpec_2, ): Iterable<CatalogProcessorResult>; -// @alpha -export const permissionRules: { - hasAnnotation: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - value?: string | undefined; - annotation: string; - } - >; - hasLabel: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - label: string; - } - >; - hasMetadata: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - value?: string | undefined; - key: string; - } - >; - hasSpec: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - value?: string | undefined; - key: string; - } - >; - isEntityKind: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - kinds: string[]; - } - >; - isEntityOwner: PermissionRule< - Entity, - EntitiesSearchFilter, - 'catalog-entity', - { - claims: string[]; - } - >; -}; - // @public export class PlaceholderProcessor implements CatalogProcessor { constructor(options: PlaceholderProcessorOptions); diff --git a/plugins/catalog-backend/migrations/20220222164811_reprocess_for_relation_refs.js b/plugins/catalog-backend/migrations/20220222164811_reprocess_for_relation_refs.js index f4be2172a0..0679d5cbdb 100644 --- a/plugins/catalog-backend/migrations/20220222164811_reprocess_for_relation_refs.js +++ b/plugins/catalog-backend/migrations/20220222164811_reprocess_for_relation_refs.js @@ -21,11 +21,11 @@ */ exports.up = async function up(knex) { // Make sure to reprocess everything, to make sure that relations have a targetRef produced + await knex('final_entities').update({ hash: '' }); 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/migrations/20221109192547_search_add_original_value_column.js b/plugins/catalog-backend/migrations/20221109192547_search_add_original_value_column.js index fff7955530..597006e6f5 100644 --- a/plugins/catalog-backend/migrations/20221109192547_search_add_original_value_column.js +++ b/plugins/catalog-backend/migrations/20221109192547_search_add_original_value_column.js @@ -34,11 +34,11 @@ exports.up = async function up(knex) { // not enough to just reset the final_entities hash, since stitching is driven // only by processing resulting in data that isn't matching the refresh_state // hash. + await knex('final_entities').update({ hash: '' }); await knex('refresh_state').update({ result_hash: '', next_update_at: knex.fn.now(), }); - await knex('final_entities').update({ hash: '' }); }; /** diff --git a/plugins/catalog-backend/migrations/20230125085746_trigger_reprocessing.js b/plugins/catalog-backend/migrations/20230125085746_trigger_reprocessing.js new file mode 100644 index 0000000000..9bd30723cd --- /dev/null +++ b/plugins/catalog-backend/migrations/20230125085746_trigger_reprocessing.js @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param { import("knex").Knex } knex + */ +exports.up = async function up(knex) { + await knex('final_entities').update({ hash: '' }); + await knex('refresh_state').update({ + result_hash: '', + next_update_at: knex.fn.now(), + }); +}; + +/** + * @param { import("knex").Knex } _knex + */ +exports.down = async function down(_knex) {}; diff --git a/plugins/catalog-backend/openapi.yaml b/plugins/catalog-backend/openapi.yaml new file mode 100644 index 0000000000..ea09f5da10 --- /dev/null +++ b/plugins/catalog-backend/openapi.yaml @@ -0,0 +1,783 @@ +openapi: 3.1.0 + +info: + title: '@backstage/plugin-catalog-backend' + version: '1' + description: The Backstage backend plugin that provides the Backstage catalog + license: + name: Apache-2.0 + contact: {} + +servers: + - url: / + +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: {} + schemas: + RefreshOptions: + type: object + properties: + authorizationToken: + type: string + entityRef: + type: string + description: The reference to a single entity that should be refreshed + required: + - entityRef + description: Options for requesting a refresh of entities in the catalog. + JsonObject: + type: object + properties: {} + description: A type representing all allowed JSON object values. + MapStringString: + type: object + properties: {} + additionalProperties: + type: string + description: Construct a type with a set of properties K of type T + EntityLink: + type: object + properties: + type: + type: string + description: An optional value to categorize links into specific groups + icon: + type: string + description: An optional semantic key that represents a visual icon. + title: + type: string + description: An optional descriptive title for the link. + url: + type: string + description: The url to the external site, document, etc. + required: + - url + description: A link to external information that is related to the entity. + EntityMeta: + allOf: + - $ref: '#/components/schemas/JsonObject' + - type: object + properties: + links: + type: array + items: + $ref: '#/components/schemas/EntityLink' + description: A list of external hyperlinks related to the entity. + tags: + type: array + items: + type: string + description: |- + A list of single-valued strings, to for example classify catalog entities in + various ways. + annotations: + $ref: '#/components/schemas/MapStringString' + labels: + $ref: '#/components/schemas/MapStringString' + description: + type: string + description: |- + A short (typically relatively few words, on one line) description of the + entity. + title: + type: string + description: |- + A display name of the entity, to be presented in user interfaces instead + of the `name` property above, when available. + This field is sometimes useful when the `name` is cumbersome or ends up + being perceived as overly technical. The title generally does not have + as stringent format requirements on it, so it may contain special + characters and be more explanatory. Do keep it very short though, and + avoid situations where a title can be confused with the name of another + entity, or where two entities share a title. + Note that this is only for display purposes, and may be ignored by some + parts of the code. Entity references still always make use of the `name` + property, not the title. + namespace: + type: string + description: The namespace that the entity belongs to. + name: + type: string + description: |- + The name of the entity. + Must be unique within the catalog at any given point in time, for any + given namespace + kind pair. This value is part of the technical + identifier of the entity, and as such it will appear in URLs, database + tables, entity references, and similar. It is subject to restrictions + regarding what characters are allowed. + If you want to use a different, more human readable string with fewer + restrictions on it in user interfaces, see the `title` field below. + etag: + type: string + description: |- + An opaque string that changes for each update operation to any part of + the entity, including metadata. + 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. The field can (optionally) be specified when performing + update or delete operations, and the server will then reject the + operation if it does not match the current stored value. + uid: + type: string + description: |- + A globally unique ID for the entity. + 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. The field can (optionally) be specified when performing + update or delete operations, but the server is free to reject requests + that do so in such a way that it breaks semantics. + required: + - name + description: Metadata fields common to all versions/kinds of entity. + EntityRelation: + type: object + properties: + targetRef: + type: string + description: The entity ref of the target of this relation. + type: + type: string + description: The type of the relation. + required: + - targetRef + - type + description: A relation of a specific type to another entity in the catalog. + Entity: + type: object + properties: + relations: + type: array + items: + $ref: '#/components/schemas/EntityRelation' + description: The relations that this entity has with other entities. + spec: + $ref: '#/components/schemas/JsonObject' + metadata: + $ref: '#/components/schemas/EntityMeta' + kind: + type: string + description: The high level entity type being described. + apiVersion: + type: string + description: |- + The version of specification format for this particular entity that + this is written against. + required: + - metadata + - kind + - apiVersion + description: The parts of the format that's common to all versions/kinds of entity. + EntityAncestryResponse: + type: object + properties: + items: + type: array + items: + type: object + properties: + parentEntityRefs: + items: + type: string + type: array + entity: + $ref: '#/components/schemas/Entity' + required: + - parentEntityRefs + - entity + rootEntityRef: + type: string + required: + - items + - rootEntityRef + EntitiesBatchResponse: + type: object + properties: + items: + type: array + items: + allOf: + - $ref: '#/components/schemas/Entity' + nullable: true + description: |- + The list of entities, in the same order as the refs in the request. Entries + that are null signify that no entity existed with that ref. + required: + - items + EntityFacets: + type: object + properties: + value: + type: string + count: + type: number + description: Construct a type with a set of properties K of type T + EntityFacetsResponse: + type: object + properties: {} + additionalProperties: + $ref: '#/components/schemas/EntityFacets' + required: + - facets + Location: + type: object + properties: + target: + type: string + type: + type: string + id: + type: string + required: + - target + - type + - id + description: Entity location for a specific entity. + LocationSpec: + type: object + properties: + presence: + type: string + enum: + - optional + - required + target: + type: string + type: + type: string + required: + - target + - type + description: Holds the entity location information. + AnalyzeLocationExistingEntity: + type: object + properties: + entity: + $ref: '#/components/schemas/Entity' + isRegistered: + type: boolean + location: + $ref: '#/components/schemas/LocationSpec' + required: + - entity + - isRegistered + - location + description: |- + 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 + RecursivePartial_Entity_: + type: object + properties: + apiVersion: + type: string + description: |- + The version of specification format for this particular entity that + this is written against. + kind: + type: string + description: The high level entity type being described. + metadata: + $ref: '#/components/schemas/EntityMeta' + spec: + $ref: '#/components/schemas/JsonObject' + relations: + type: array + items: + $ref: '#/components/schemas/EntityRelation' + description: The relations that this entity has with other entities. + description: Makes all keys of an entire hierarchy optional. + AnalyzeLocationEntityField: + type: object + properties: + description: + type: string + description: |- + 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. + value: + type: string + nullable: true + state: + type: string + enum: + - analysisSuggestedValue + - analysisSuggestedNoValue + - needsUserInput + description: The outcome of the analysis for this particular field + field: + type: string + description: |- + 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 + required: + - description + - value + - state + - field + AnalyzeLocationGenerateEntity: + type: object + properties: + fields: + type: array + items: + $ref: '#/components/schemas/AnalyzeLocationEntityField' + entity: + $ref: '#/components/schemas/RecursivePartial_Entity_' + required: + - fields + - entity + description: |- + 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. + AnalyzeLocationResponse: + type: object + properties: + generateEntities: + items: + $ref: '#/components/schemas/AnalyzeLocationGenerateEntity' + type: array + existingEntityFiles: + items: + $ref: '#/components/schemas/AnalyzeLocationExistingEntity' + type: array + required: + - generateEntities + - existingEntityFiles + LocationInput: + type: object + properties: + type: + type: string + target: + type: string + presence: + type: string + enum: + - optional + - required + required: + - type + - target + - presence + SerializedError: + allOf: + - $ref: '#/components/schemas/JsonObject' + - type: object + properties: + code: + type: string + description: A custom code (not necessarily the same as an HTTP response code); may not be present + stack: + type: string + description: A stringified stack trace; may not be present + message: + type: string + description: The message of the exception that was thrown + name: + type: string + description: The name of the exception that was thrown + required: + - message + - name + description: The serialized form of an Error. + securitySchemes: + JWT: + type: http + scheme: bearer + bearerFormat: JWT + +paths: + /refresh: + post: + operationId: RefreshEntity + responses: + '200': + description: Refreshed + security: + # From https://stackoverflow.com/questions/47659324/how-to-specify-an-endpoints-authorization-is-optional-in-openapi-v3 + - {} + - JWT: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RefreshOptions' + + /entities: + get: + operationId: GetEntities + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Entity' + security: + - {} + - JWT: [] + parameters: + - in: query + name: filter + required: false + schema: + type: string + - in: query + name: fields + required: false + schema: + type: string + - in: query + name: offset + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: string + - in: query + name: after + required: false + schema: + type: string + + /entities/by-uid/{uid}: + get: + operationId: GetEntityByUid + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Entity' + security: + - {} + - JWT: [] + parameters: + - in: path + name: uid + required: true + schema: + type: string + delete: + operationId: DeleteEntityByUid + responses: + '204': + description: Deleted successfully. + security: + - {} + - JWT: [] + parameters: + - in: path + name: uid + required: true + schema: + type: string + + /entities/by-name/{kind}/{namespace}/{name}: + get: + operationId: GetEntityByName + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Entity' + security: + - {} + - JWT: [] + parameters: + - in: path + name: kind + required: true + schema: + type: string + - in: path + name: namespace + required: true + schema: + type: string + - in: path + name: name + required: true + schema: + type: string + + /entities/by-name/{kind}/{namespace}/{name}/ancestry: + get: + operationId: GetEntityAncestryByName + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/EntityAncestryResponse' + security: + - {} + - JWT: [] + parameters: + - in: path + name: kind + required: true + schema: + type: string + - in: path + name: namespace + required: true + schema: + type: string + - in: path + name: name + required: true + schema: + type: string + + /entities/by-refs: + post: + operationId: GetEntitiesByRefs + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/EntitiesBatchResponse' + security: + - {} + - JWT: [] + parameters: + - in: query + name: fields + required: false + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: string + + /entity-facets: + get: + operationId: GetEntityFacets + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/EntityFacetsResponse' + security: + - {} + - JWT: [] + parameters: + - in: query + name: facet + required: true + schema: + type: string + - in: query + name: filter + required: false + schema: + type: string + + /locations: + post: + operationId: CreateLocation + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + exists: + type: boolean + entities: + items: + $ref: '#/components/schemas/Entity' + type: array + location: + $ref: '#/components/schemas/Location' + required: + - entities + - location + security: + - {} + - JWT: [] + parameters: + - in: query + name: dryRun + required: false + schema: + type: boolean + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + presence: + type: string + enum: + - required + - optional + target: + type: string + type: + type: string + required: + - presence + - target + - type + get: + operationId: GetLocations + responses: + '200': + description: Ok + content: + application/json: + schema: + type: array + items: + type: object + properties: + data: + $ref: '#/components/schemas/Location' + required: + - data + security: + - {} + - JWT: [] + parameters: [] + + /locations/{id}: + get: + operationId: GetLocation + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + security: + - {} + - JWT: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + delete: + operationId: DeleteLocation + responses: + '204': + description: No content + security: + - {} + - JWT: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + + /analyze-location: + post: + operationId: AnalyzeLocation + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/AnalyzeLocationResponse' + security: + - {} + - JWT: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + catalogFileName: + type: string + location: + $ref: '#/components/schemas/LocationInput' + required: + - catalogFileName + - location + + /validate-entity: + post: + operationId: ValidateEntity + responses: + '200': + description: Ok + content: + application/json: + schema: + anyOf: + - type: object + properties: + errors: + $ref: '#/components/schemas/SerializedError' + required: + - errors + - type: object + properties: + errors: + type: array + items: + $ref: '#/components/schemas/SerializedError' + required: + - errors + security: + - {} + - JWT: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + location: + type: string + entity: + $ref: '#/components/schemas/Entity' + required: + - location + - entity diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index e1dad7589a..5d1be38754 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "1.7.0", + "version": "1.8.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -82,13 +94,12 @@ "@types/uuid": "^8.0.0", "better-sqlite3": "^8.0.0", "luxon": "^3.0.0", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2" }, "files": [ "dist", - "alpha", "migrations/**/*.{js,d.ts}", "config.d.ts" ], diff --git a/plugins/catalog-backend/src/alpha.ts b/plugins/catalog-backend/src/alpha.ts new file mode 100644 index 0000000000..aa1309aecd --- /dev/null +++ b/plugins/catalog-backend/src/alpha.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO(Rugvip): Re-exported for alpha types as the API report will otherwise +// produce warnings due to the indirect dependency. We be nice to avoid. +import type { EntitiesSearchFilter } from './catalog/types'; + +export type { /** @alpha */ EntitiesSearchFilter }; + +export * from './permissions'; +export { catalogPlugin } from './service/CatalogPlugin'; diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index faf7451acd..404b816d4f 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -98,7 +98,9 @@ export interface EntitiesBatchRequest { */ entityRefs: string[]; /** - * Any additional filters to apply in the selection of the entities. + * Any additional filters to apply in the selection of the entities. Entities + * that do not match the filter result in a null entry in the response, as if + * they did not exist. */ filter?: EntityFilter; /** @@ -174,6 +176,13 @@ export interface EntitiesCatalog { */ entitiesBatch(request: EntitiesBatchRequest): Promise<EntitiesBatchResponse>; + /** + * Fetch entities and scroll back and forth between entities. + * + * @param request + */ + queryEntities(request: QueryEntitiesRequest): Promise<QueryEntitiesResponse>; + /** * Removes a single entity. * @@ -202,3 +211,103 @@ export interface EntitiesCatalog { */ facets(request: EntityFacetsRequest): Promise<EntityFacetsResponse>; } + +/** + * The request shape for {@link EntitiesCatalog.queryEntities}. + */ +export type QueryEntitiesRequest = + | QueryEntitiesInitialRequest + | QueryEntitiesCursorRequest; + +/** + * The initial request for {@link EntitiesCatalog.queryEntities}. + * The request take immutable properties that are going to be bound + * for the current and the next pagination requests. + */ +export interface QueryEntitiesInitialRequest { + authorizationToken?: string; + fields?: (entity: Entity) => Entity; + limit?: number; + filter?: EntityFilter; + orderFields?: EntityOrder[]; + fullTextFilter?: { + term: string; + fields?: string[]; + }; +} + +/** + * Request for {@link EntitiesCatalog.queryEntities} used to + * move forward or backward on the data. + */ +export interface QueryEntitiesCursorRequest { + authorizationToken?: string; + fields?: (entity: Entity) => Entity; + limit?: number; + cursor: Cursor; +} + +/** + * The response shape for {@link EntitiesCatalog.queryEntities}. + */ +export interface QueryEntitiesResponse { + /** + * The entities for the current pagination request + */ + items: Entity[]; + + pageInfo: { + /** + * The cursor of the next pagination request. + */ + nextCursor?: Cursor; + /** + * The cursor of the previous pagination request. + */ + prevCursor?: Cursor; + }; + /** + * the total number of entities matching the current filters. + */ + totalItems: number; +} + +/** + * The Cursor used internally by the catalog. + */ +export type Cursor = { + /** + * An array of fields used for sorting the data. + * For example, [ { field: 'metadata.name', order: 'asc' } ] + */ + orderFields: EntityOrder[]; + /** + * The values of the fields of a specific item used for paginating the data. + */ + orderFieldValues: Array<string | null>; + /** + * A filter to be applied to the full list of entities. + */ + filter?: EntityFilter; + /** + * true if the cursor is a previous cursor. + */ + isPrevious: boolean; + /** + * Used for performing full text filtering on the given fields. + */ + fullTextFilter?: { + term: string; + fields?: string[]; + }; + /** + * The value of the fields of the first returned item used for paginating the data. + * The catalog uses this field internally to understand if the beginning + * of the list has been reached when performing cursor based pagination. + */ + firstSortFieldValues?: Array<string | null>; + /** + * The number of items that match the provided filters + */ + totalItems?: number; +}; diff --git a/plugins/catalog-backend/src/database/DefaultCatalogDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultCatalogDatabase.test.ts index b873f6a985..827ab58ebe 100644 --- a/plugins/catalog-backend/src/database/DefaultCatalogDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultCatalogDatabase.test.ts @@ -105,6 +105,7 @@ describe('DefaultCatalogDatabase', () => { 'location:default/root-2', ]); }, + 60_000, ); }); }); diff --git a/plugins/catalog-backend/src/database/DefaultProviderDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultProviderDatabase.test.ts index 140925b6fd..09bec187fa 100644 --- a/plugins/catalog-backend/src/database/DefaultProviderDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultProviderDatabase.test.ts @@ -745,5 +745,39 @@ describe('DefaultProviderDatabase', () => { }, 60_000, ); + + it.each(databases.eachSupportedId())( + 'should gracefully handle accidental duplicate refresh state references when deletion happens during a full sync, %p', + async databaseId => { + const fakeLogger = { debug: jest.fn() }; + const { knex, db } = await createDatabase( + databaseId, + fakeLogger as any, + ); + + await createLocations(knex, ['component:default/a']); + + await insertRefRow(knex, { + source_key: 'a', + target_entity_ref: 'component:default/a', + }); + await insertRefRow(knex, { + source_key: 'a', + target_entity_ref: 'component:default/a', + }); + + await db.transaction(async tx => { + await db.replaceUnprocessedEntities(tx, { + type: 'full', + sourceKey: 'a', + items: [], + }); + }); + + const state = await knex<DbRefreshStateRow>('refresh_state').select(); + expect(state).toEqual([]); + }, + 60_000, + ); }); }); diff --git a/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.test.ts b/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.test.ts new file mode 100644 index 0000000000..8ab94df64a --- /dev/null +++ b/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.test.ts @@ -0,0 +1,278 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Knex } from 'knex'; +import * as uuid from 'uuid'; +import { applyDatabaseMigrations } from '../../migrations'; +import { DbRefreshStateReferencesRow, DbRefreshStateRow } from '../../tables'; +import { deleteWithEagerPruningOfChildren } from './deleteWithEagerPruningOfChildren'; + +describe('deleteWithEagerPruningOfChildren', () => { + const databases = TestDatabases.create({ + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + async function createDatabase(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + await applyDatabaseMigrations(knex); + return knex; + } + + async function run( + knex: Knex, + options: { entityRefs: string[]; sourceKey: string }, + ): Promise<number> { + let result: number; + await knex.transaction( + async tx => { + // We can't return here, as knex swallows the return type in case the + // transaction is rolled back: + // https://github.com/knex/knex/blob/e37aeaa31c8ef9c1b07d2e4d3ec6607e557d800d/lib/transaction.js#L136 + result = await deleteWithEagerPruningOfChildren({ tx, ...options }); + }, + { + // If we explicitly trigger a rollback, don't fail. + doNotRejectOnRollback: true, + }, + ); + return result!; + } + + async function insertReference( + knex: Knex, + ...refs: DbRefreshStateReferencesRow[] + ) { + return knex<DbRefreshStateReferencesRow>('refresh_state_references').insert( + refs, + ); + } + + async function insertEntity(knex: Knex, ...entityRefs: string[]) { + for (const ref of entityRefs) { + await knex<DbRefreshStateRow>('refresh_state').insert({ + entity_id: uuid.v4(), + entity_ref: ref, + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: '2021-04-01 13:37:00', + last_discovery_at: '2021-04-01 13:37:00', + }); + } + } + + async function remainingEntities(knex: Knex) { + const rows = await knex<DbRefreshStateRow>('refresh_state') + .orderBy('entity_ref') + .select('entity_ref'); + return rows.map(r => r.entity_ref); + } + + it.each(databases.eachSupportedId())( + 'works for the simple path, %p', + async databaseId => { + /* + P1 - E1 - E2 + + P1 - E3 + + P1 - E4 + + P2 - E5 + + Scenario: P1 issues delete for E1 and E3 + + Result: E1, E2, and E3 deleted; E4 and E5 remain + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3', 'E4', 'E5'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_key: 'P1', target_entity_ref: 'E3' }, + { source_key: 'P1', target_entity_ref: 'E4' }, + { source_key: 'P2', target_entity_ref: 'E5' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1', 'E3'] }); + await expect(remainingEntities(knex)).resolves.toEqual(['E4', 'E5']); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'works when there are multiple identical references, %p', + async databaseId => { + /* + P1 + \ + E1 + / + P1 + + P1 - E2 + + Scenario: P1 issues delete for E1 + + Result: E1 deleted; E2 remains + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_key: 'P1', target_entity_ref: 'E2' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1'] }); + await expect(remainingEntities(knex)).resolves.toEqual(['E2']); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'leaves out things that have roots in other source keys, %p', + async databaseId => { + /* + P1 - E1 + \ + E2 + / + P2 - E3 + + Scenario: P1 issues delete for E1 + + Result: E1 deleted; E2 and E3 remain + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_key: 'P2', target_entity_ref: 'E3' }, + { source_entity_ref: 'E3', target_entity_ref: 'E2' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1'] }); + await expect(remainingEntities(knex)).resolves.toEqual(['E2', 'E3']); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'leaves out things that have several different roots for the same source key, %p', + async databaseId => { + /* + P1 - E1 + \ + E2 + / + P1 - E3 + + Scenario: P1 issues delete for E1 + + Result: E1 deleted; E2 and E3 remain + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_key: 'P1', target_entity_ref: 'E3' }, + { source_entity_ref: 'E3', target_entity_ref: 'E2' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1'] }); + await expect(remainingEntities(knex)).resolves.toEqual(['E2', 'E3']); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'handles cycles and diamonds gracefully, %p', + async databaseId => { + /* + P1 - E1 <-> E2 + \ + E4 E6 + / \ / + P1 -- E3 -- E5 + + Scenario: P1 issues delete for E1, then for E3 + + Result: Everything deleted, but in two steps + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3', 'E4', 'E5', 'E6'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_entity_ref: 'E2', target_entity_ref: 'E1' }, + { source_entity_ref: 'E2', target_entity_ref: 'E6' }, + { source_key: 'P1', target_entity_ref: 'E3' }, + { source_entity_ref: 'E3', target_entity_ref: 'E4' }, + { source_entity_ref: 'E4', target_entity_ref: 'E5' }, + { source_entity_ref: 'E3', target_entity_ref: 'E5' }, + { source_entity_ref: 'E5', target_entity_ref: 'E6' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1'] }); + await expect(remainingEntities(knex)).resolves.toEqual([ + 'E3', + 'E4', + 'E5', + 'E6', + ]); + await run(knex, { sourceKey: 'P1', entityRefs: ['E3'] }); + await expect(remainingEntities(knex)).resolves.toEqual([]); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'silently ignores attempts to delete things that are not your own and/or are not roots, %p', + async databaseId => { + /* + P1 - E1 - E2 + + P1 - E3 + + P2 - E4 + + Scenario: P1 issues delete for E2, E3, and E4 + + Result: E3 is deleted; E1, E2 and E4 remain + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3', 'E4'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_key: 'P1', target_entity_ref: 'E3' }, + { source_key: 'P2', target_entity_ref: 'E4' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E2', 'E3', 'E4'] }); + await expect(remainingEntities(knex)).resolves.toEqual([ + 'E1', + 'E2', + 'E4', + ]); + }, + 60_000, + ); +}); diff --git a/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.ts b/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.ts index 3d642cbf3a..33da37bc55 100644 --- a/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.ts +++ b/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.ts @@ -16,7 +16,7 @@ import { Knex } from 'knex'; import lodash from 'lodash'; -import { DbRefreshStateReferencesRow, DbRefreshStateRow } from '../../tables'; +import { DbRefreshStateReferencesRow } from '../../tables'; /** * Given a number of entity refs originally created by a given entity provider @@ -31,116 +31,157 @@ export async function deleteWithEagerPruningOfChildren(options: { sourceKey: string; }): Promise<number> { const { tx, entityRefs, sourceKey } = options; - let removedCount = 0; - - const rootId = () => - tx.raw( - tx.client.config.client.includes('mysql') - ? 'CAST(NULL as UNSIGNED INT)' - : 'CAST(NULL as INT)', - [], - ); // Split up the operation by (large) chunks, so that we do not hit database // limits for the number of permitted bindings on a precompiled statement + let removedCount = 0; for (const refs of lodash.chunk(entityRefs, 1000)) { - /* - WITH RECURSIVE - -- All the nodes that can be reached downwards from our root - descendants(root_id, entity_ref) AS ( - SELECT id, target_entity_ref - FROM refresh_state_references - WHERE source_key = "R1" AND target_entity_ref = "A" - UNION - SELECT descendants.root_id, target_entity_ref - FROM descendants - JOIN refresh_state_references ON source_entity_ref = descendants.entity_ref - ), - -- All the nodes that can be reached upwards from the descendants - ancestors(root_id, via_entity_ref, to_entity_ref) AS ( - SELECT CAST(NULL as INT), entity_ref, entity_ref - FROM descendants - UNION - SELECT - CASE WHEN source_key IS NOT NULL THEN id ELSE NULL END, - source_entity_ref, - ancestors.to_entity_ref - FROM ancestors - JOIN refresh_state_references ON target_entity_ref = ancestors.via_entity_ref - ) - -- Start out with all of the descendants - SELECT descendants.entity_ref - FROM descendants - -- Expand with all ancestors that point to those, but aren't the current root - LEFT OUTER JOIN ancestors - ON ancestors.to_entity_ref = descendants.entity_ref - AND ancestors.root_id IS NOT NULL - AND ancestors.root_id != descendants.root_id - -- Exclude all lines that had such a foreign ancestor - WHERE ancestors.root_id IS NULL; - */ - removedCount += await tx<DbRefreshStateRow>('refresh_state') - .whereIn('entity_ref', function orphanedEntityRefs(orphans) { - return ( - orphans - // All the nodes that can be reached downwards from our root - .withRecursive('descendants', function descendants(outer) { - return outer - .select({ root_id: 'id', entity_ref: 'target_entity_ref' }) - .from('refresh_state_references') - .where('source_key', sourceKey) - .whereIn('target_entity_ref', refs) - .union(function recursive(inner) { - return inner - .select({ - root_id: 'descendants.root_id', - entity_ref: 'refresh_state_references.target_entity_ref', - }) - .from('descendants') - .join('refresh_state_references', { - 'descendants.entity_ref': - 'refresh_state_references.source_entity_ref', - }); - }); - }) - // All the nodes that can be reached upwards from the descendants - .withRecursive('ancestors', function ancestors(outer) { - return outer - .select({ - root_id: rootId(), - via_entity_ref: 'entity_ref', - to_entity_ref: 'entity_ref', - }) + removedCount += await tx + .delete() + .from('refresh_state') + .whereIn('entity_ref', orphans => + orphans + // First find all nodes that can be reached downwards from the roots + // (deletion targets), including the roots themselves, by traversing + // down the refresh_state_references table. Note that this query + // starts with a condition that source_key = our source key, and + // target_entity_ref is one of the deletion targets. This has two + // effects: it won't match attempts at deleting something that didn't + // originate from us in the first place, and also won't match non-root + // entities (source_key would be null for those). + // + // KeyA - R1 - R2 Legend: + // \ ----------------------------------------- + // R3 Key* Source key + // / R* Entity ref + // KeyA - R4 - R5 lines Individual references; sources to + // / the left and targets to the right + // KeyB --- R6 + // + // The scenario is that KeyA wants to delete R1. + // + // The query starts with the KeyA-R1 reference, and then traverses + // down to also find R2 and R3. It uses union instead of union all, + // because it wants to find the set of unique descendants even if + // the tree has unexpected loops etc. + .withRecursive('descendants', ['entity_ref'], initial => + initial + .select('target_entity_ref') + .from('refresh_state_references') + .where('source_key', '=', sourceKey) + .whereIn('target_entity_ref', refs) + .union(recursive => + recursive + .select('refresh_state_references.target_entity_ref') + .from('descendants') + .join( + 'refresh_state_references', + 'descendants.entity_ref', + 'refresh_state_references.source_entity_ref', + ), + ), + ) + // Then for each descendant, traverse all the way back upwards through + // the refresh_state_references table to get an exhaustive list of all + // references that are part of keeping that particular descendant + // alive. + // + // Continuing the scenario from above, starting from R3, it goes + // upwards to find every pair along every relation line. + // + // Top branch: R2-R3, R1-R2, KeyA-R1 + // Middle branch: R5-R3, R4-R5, KeyA-R4 + // Bottom branch: R6-R5, KeyB-R6 + // + // Note that this all applied to the subject R3. The exact same thing + // will be done starting from each other descendant (R2 and R1). They + // only have one and two references to find, respectively. + // + // This query also uses union instead of union all, to get the set of + // distinct relations even if the tree has unexpected loops etc. + .withRecursive( + 'ancestors', + ['source_key', 'source_entity_ref', 'target_entity_ref', 'subject'], + initial => + initial + .select( + 'refresh_state_references.source_key', + 'refresh_state_references.source_entity_ref', + 'refresh_state_references.target_entity_ref', + 'descendants.entity_ref', + ) .from('descendants') - .union(function recursive(inner) { - return inner - .select({ - root_id: tx.raw( - 'CASE WHEN source_key IS NOT NULL THEN id ELSE NULL END', - [], - ), - via_entity_ref: 'source_entity_ref', - to_entity_ref: 'ancestors.to_entity_ref', - }) + .join( + 'refresh_state_references', + 'refresh_state_references.target_entity_ref', + 'descendants.entity_ref', + ) + .union(recursive => + recursive + .select( + 'refresh_state_references.source_key', + 'refresh_state_references.source_entity_ref', + 'refresh_state_references.target_entity_ref', + 'ancestors.subject', + ) .from('ancestors') - .join('refresh_state_references', { - target_entity_ref: 'ancestors.via_entity_ref', - }); - }); - }) - // Start out with all of the descendants - .select('descendants.entity_ref') - .from('descendants') - // Expand with all ancestors that point to those, but aren't the current root - .leftOuterJoin('ancestors', function keepaliveRoots() { - this.on('ancestors.to_entity_ref', '=', 'descendants.entity_ref'); - this.andOnNotNull('ancestors.root_id'); - this.andOn('ancestors.root_id', '!=', 'descendants.root_id'); - }) - .whereNull('ancestors.root_id') - ); - }) - .delete(); + .join( + 'refresh_state_references', + 'refresh_state_references.target_entity_ref', + 'ancestors.source_entity_ref', + ), + ), + ) + // Finally, from that list of ancestor relations per descendant, pick + // out the ones that are roots (have a source_key). Specifically, find + // ones that seem to be be either (1) from another source, or (2) + // aren't part of the deletion targets. Those are markers that tell us + // that the corresponding descendant should be kept alive and NOT + // subject to eager deletion, because there's "something else" (not + // targeted for deletion) that has references down through the tree to + // it. + // + // Continuing the scenario from above, for R3 we have + // + // KeyA-R1, KeyA-R4, KeyB-R6 + // + // This tells us that R3 should be kept alive for two reasons: it's + // referenced by a node that isn't being deleted (R4), and also by + // another source (KeyB). What about R1 and R2? They both have + // + // KeyA-R1 + // + // So those should be deleted, since they are definitely only being + // kept alive by something that's about to be deleted. + // + // Final shape of the tree: + // + // R3 + // / + // KeyA - R4 - R5 + // / + // KeyB --- R6 + .with('retained', ['entity_ref'], notPartOfDeletion => + notPartOfDeletion + .select('subject') + .from('ancestors') + .whereNotNull('ancestors.source_key') + .where(foreignKeyOrRef => + foreignKeyOrRef + .where('ancestors.source_key', '!=', sourceKey) + .orWhereNotIn('ancestors.target_entity_ref', refs), + ), + ) + // Return all descendants minus the retained ones + .select('descendants.entity_ref') + .from('descendants') + .leftOuterJoin( + 'retained', + 'retained.entity_ref', + 'descendants.entity_ref', + ) + .whereNull('retained.entity_ref'), + ); // Delete the references that originate only from this entity provider. Note // that there may be more than one entity provider making a "claim" for a diff --git a/plugins/catalog-backend/src/index.ts b/plugins/catalog-backend/src/index.ts index 000dd94e8b..e52336f036 100644 --- a/plugins/catalog-backend/src/index.ts +++ b/plugins/catalog-backend/src/index.ts @@ -42,7 +42,6 @@ export { processingResult } from '@backstage/plugin-catalog-node'; export * from './catalog'; export * from './ingestion'; export * from './modules'; -export * from './permissions'; export * from './processing'; export * from './search'; export * from './service'; diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index 34e89fad29..592b1dda21 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -35,6 +35,10 @@ const entity = { }; const location: Record<string, LocationSpec> = { + v: { + type: 'url', + target: 'https://github.com/b/c/blob/master/.folder/v.yaml', + }, w: { type: 'url', target: 'https://github.com/b/c/blob/master/w.yaml', @@ -263,4 +267,20 @@ describe('DefaultCatalogRulesEnforcer', () => { expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); }); }); + + it('should allow locations with a hidden folder', () => { + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( + new ConfigReader({ + catalog: { + rules: [ + { + allow: ['Component'], + locations: [{ type: 'url', pattern: 'https://github.com/b/**' }], + }, + ], + }, + }), + ); + expect(enforcer.isAllowed(entity.component, location.v)).toBe(true); + }); }); diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index 677649d4b2..21b2d05a46 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -186,7 +186,10 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { } if ( matcher.pattern && - !minimatch(location?.target, matcher.pattern, { nocase: true }) + !minimatch(location?.target, matcher.pattern, { + nocase: true, + dot: true, + }) ) { continue; } diff --git a/plugins/catalog-backend/src/integration.test.ts b/plugins/catalog-backend/src/integration.test.ts index 4ba046834b..b13e6ef9de 100644 --- a/plugins/catalog-backend/src/integration.test.ts +++ b/plugins/catalog-backend/src/integration.test.ts @@ -52,9 +52,14 @@ import { } from '@backstage/plugin-catalog-node'; import { RefreshStateItem } from './database/types'; import { DefaultProviderDatabase } from './database/DefaultProviderDatabase'; +import { InputError } from '@backstage/errors'; const voidLogger = getVoidLogger(); +type ProgressTrackerWithErrorReports = ProgressTracker & { + reportError(unprocessedEntity: Entity, errors: Error[]): void; +}; + class TestProvider implements EntityProvider { #connection?: EntityProviderConnection; @@ -74,10 +79,10 @@ class TestProvider implements EntityProvider { } } -class ProxyProgressTracker implements ProgressTracker { - #inner: ProgressTracker; +class ProxyProgressTracker implements ProgressTrackerWithErrorReports { + #inner: ProgressTrackerWithErrorReports; - constructor(inner: ProgressTracker) { + constructor(inner: ProgressTrackerWithErrorReports) { this.#inner = inner; } @@ -85,12 +90,16 @@ class ProxyProgressTracker implements ProgressTracker { return this.#inner.processStart(item, voidLogger); } - setTracker(tracker: ProgressTracker) { + setTracker(tracker: ProgressTrackerWithErrorReports) { this.#inner = tracker; } + + reportError(unprocessedEntity: Entity, errors: Error[]): void { + this.#inner.reportError(unprocessedEntity, errors); + } } -class NoopProgressTracker implements ProgressTracker { +class NoopProgressTracker implements ProgressTrackerWithErrorReports { static emptyTracking = { markFailed() {}, markProcessorsCompleted() {}, @@ -102,18 +111,20 @@ class NoopProgressTracker implements ProgressTracker { processStart() { return NoopProgressTracker.emptyTracking; } + + reportError() {} } -class WaitingProgressTracker implements ProgressTracker { - #resolve: (errors: Record<string, Error>) => void; - #promise: Promise<Record<string, Error>>; +class WaitingProgressTracker implements ProgressTrackerWithErrorReports { + #resolve: (errors: Record<string, Error[]>) => void; + #promise: Promise<Record<string, Error[]>>; #counts = new Map<string, number>(); - #errors = new Map<string, Error>(); + #errors = new Map<string, Error[]>(); #inFlight = new Array<Promise<void>>(); constructor(private readonly entityRefs?: Set<string>) { - let resolve: (errors: Record<string, Error>) => void; - this.#promise = new Promise<Record<string, Error>>(_resolve => { + let resolve: (errors: Record<string, Error[]>) => void; + this.#promise = new Promise<Record<string, Error[]>>(_resolve => { resolve = _resolve; }); this.#resolve = resolve!; @@ -143,7 +154,7 @@ class WaitingProgressTracker implements ProgressTracker { }; return { markFailed: (error: Error) => { - this.#errors.set(item.entityRef, error); + this.#errors.set(item.entityRef, [error]); onDone(); resolve(); }, @@ -154,7 +165,6 @@ class WaitingProgressTracker implements ProgressTracker { resolve(); }, markSuccessfulWithErrors: () => { - this.#errors.delete(item.entityRef); onDone(); resolve(); }, @@ -165,7 +175,11 @@ class WaitingProgressTracker implements ProgressTracker { }; } - async wait(): Promise<Record<string, Error>> { + reportError(unprocessedEntity: Entity, errors: Error[]): void { + this.#errors.set(stringifyEntityRef(unprocessedEntity), errors); + } + + async wait(): Promise<Record<string, Error[]>> { return this.#promise; } @@ -191,10 +205,6 @@ class TestHarness { location: LocationSpec, emit: CatalogProcessorEmit, ): Promise<Entity>; - onProcessingError?(event: { - unprocessedEntity: Entity; - errors: Error[]; - }): void; }) { const config = new ConfigReader( options?.config ?? { @@ -258,7 +268,11 @@ class TestHarness { legacySingleProcessorValidation: false, }); const stitcher = new Stitcher(db, logger); - const catalog = new DefaultEntitiesCatalog(db, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: db, + logger, + stitcher, + }); const proxyProgressTracker = new ProxyProgressTracker( new NoopProgressTracker(), ); @@ -271,13 +285,7 @@ class TestHarness { () => createHash('sha1'), 50, event => { - if (options?.onProcessingError) { - options.onProcessingError(event); - } else { - throw new Error( - `Catalog processing error, ${event.errors.join(', ')}`, - ); - } + proxyProgressTracker.reportError(event.unprocessedEntity, event.errors); }, proxyProgressTracker, ); @@ -388,7 +396,13 @@ describe('Catalog Backend Integration', () => { triggerError = true; - await expect(harness.process()).resolves.toEqual({}); + await expect(harness.process()).resolves.toEqual({ + 'component:default/test': [ + new InputError( + 'Processor Object threw an error while preprocessing; caused by Error: NOPE', + ), + ], + }); await expect(harness.getOutputEntities()).resolves.toEqual({ 'component:default/test': { @@ -685,4 +699,69 @@ describe('Catalog Backend Integration', () => { .annotations!['backstage.io/orphan'], ).toBeUndefined(); }); + + it('should reject insecure URLs', async () => { + const harness = await TestHarness.create(); + + await harness.setInputEntities([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'backstage.io/managed-by-location': 'url:.', + 'backstage.io/managed-by-origin-location': 'url:.', + 'backstage.io/view-url': ' javascript:bad()', + 'backstage.io/edit-url': ' javascript:alert()', + }, + }, + }, + ]); + + await expect(harness.process()).resolves.toEqual({}); + + await expect(harness.getOutputEntities()).resolves.toEqual({ + 'component:default/test': { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: expect.objectContaining({ + name: 'test', + annotations: expect.objectContaining({ + 'backstage.io/view-url': + 'https://backstage.io/annotation-rejected-for-security-reasons', + 'backstage.io/edit-url': + 'https://backstage.io/annotation-rejected-for-security-reasons', + }), + }), + relations: [], + }, + }); + }); + + it('should reject insecure location URLs', async () => { + const harness = await TestHarness.create(); + + await harness.setInputEntities([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'backstage.io/managed-by-location': 'url:javascript:bad()', + 'backstage.io/managed-by-origin-location': 'url:javascript:alert()', + }, + }, + }, + ]); + + await expect(harness.process()).resolves.toEqual({ + 'component:default/test': [ + new TypeError( + "Invalid location ref 'url:javascript:bad()', target is a javascript: URL", + ), + ], + }); + }); }); diff --git a/plugins/catalog-backend/src/permissions/conditionExports.ts b/plugins/catalog-backend/src/permissions/conditionExports.ts index 1c1ba70631..c9406cdc0e 100644 --- a/plugins/catalog-backend/src/permissions/conditionExports.ts +++ b/plugins/catalog-backend/src/permissions/conditionExports.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { createConditionExports } from '@backstage/plugin-permission-node'; import { permissionRules } from './rules'; diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts index 5b34c98ca6..b2630f0f73 100644 --- a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts @@ -25,7 +25,7 @@ describe('createPropertyRule', () => { it('formats the rule description correctly', () => { expect(description).toBe( - 'Allow entities which have the specified metadata subfield.', + 'Allow entities with the specified metadata subfield', ); }); diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts index 19a4423846..73f40da8c8 100644 --- a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts @@ -15,14 +15,14 @@ */ import { get } from 'lodash'; -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { createCatalogPermissionRule } from './util'; import { z } from 'zod'; export const createPropertyRule = (propertyType: 'metadata' | 'spec') => createCatalogPermissionRule({ name: `HAS_${propertyType.toUpperCase()}`, - description: `Allow entities which have the specified ${propertyType} subfield.`, + description: `Allow entities with the specified ${propertyType} subfield`, resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ key: z diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts index c24620f41c..315d7fa8d0 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { z } from 'zod'; import { createCatalogPermissionRule } from './util'; @@ -28,8 +28,7 @@ import { createCatalogPermissionRule } from './util'; */ export const hasAnnotation = createCatalogPermissionRule({ name: 'HAS_ANNOTATION', - description: - 'Allow entities which are annotated with the specified annotation', + description: 'Allow entities with the specified annotation', resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ annotation: z.string().describe('Name of the annotation to match on'), diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts index 376534ca6f..2d6289dd26 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { z } from 'zod'; import { createCatalogPermissionRule } from './util'; @@ -25,10 +25,10 @@ import { createCatalogPermissionRule } from './util'; */ export const hasLabel = createCatalogPermissionRule({ name: 'HAS_LABEL', - description: 'Allow entities which have the specified label metadata.', + description: 'Allow entities with the specified label', resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ - label: z.string().describe('Name of the label to match one'), + label: z.string().describe('Name of the label to match on'), }), apply: (resource, { label }) => !!resource.metadata.labels?.hasOwnProperty(label), diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts index eee7a790b2..8ebad5521c 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { z } from 'zod'; import { EntitiesSearchFilter } from '../../catalog/types'; import { createCatalogPermissionRule } from './util'; @@ -25,7 +25,7 @@ import { createCatalogPermissionRule } from './util'; */ export const isEntityKind = createCatalogPermissionRule({ name: 'IS_ENTITY_KIND', - description: 'Allow entities with the specified kind', + description: 'Allow entities matching a specified kind', resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ kinds: z diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts index c3bfff5c2e..86493315dc 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -15,7 +15,7 @@ */ import { RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { z } from 'zod'; import { createCatalogPermissionRule } from './util'; @@ -27,7 +27,7 @@ import { createCatalogPermissionRule } from './util'; */ export const isEntityOwner = createCatalogPermissionRule({ name: 'IS_ENTITY_OWNER', - description: 'Allow entities owned by the current user', + description: 'Allow entities owned by a specified claim', resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ claims: z diff --git a/plugins/catalog-backend/src/permissions/rules/util.ts b/plugins/catalog-backend/src/permissions/rules/util.ts index ea25e115d2..466f5ce70b 100644 --- a/plugins/catalog-backend/src/permissions/rules/util.ts +++ b/plugins/catalog-backend/src/permissions/rules/util.ts @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; import { makeCreatePermissionRule, diff --git a/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts b/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts new file mode 100644 index 0000000000..86d71d4c13 --- /dev/null +++ b/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; + +/** @public */ +export type CatalogCollatorEntityTransformer = ( + entity: Entity, +) => Omit<CatalogEntityDocument, 'location' | 'authorization'>; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 70ec0ee948..42c6930422 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -29,10 +29,8 @@ import { CatalogClient, GetEntitiesRequest, } from '@backstage/catalog-client'; -import { - catalogEntityReadPermission, - CatalogEntityDocument, -} from '@backstage/plugin-catalog-common'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha'; +import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; import { Permission } from '@backstage/plugin-permission-common'; /** diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts index 8b4e5601b4..462e777dff 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts @@ -95,11 +95,6 @@ describe('DefaultCatalogCollatorFactory', () => { ); }); - it('has expected type', () => { - const factory = DefaultCatalogCollatorFactory.fromConfig(config, options); - expect(factory.type).toBe('software-catalog'); - }); - describe('getCollator', () => { let factory: DefaultCatalogCollatorFactory; let collator: Readable; @@ -124,24 +119,28 @@ describe('DefaultCatalogCollatorFactory', () => { const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); - expect(documents[0]).toMatchObject({ + expect(documents[0]).toEqual({ title: expectedEntities[0].metadata.name, location: '/catalog/default/component/test-entity', text: expectedEntities[0].metadata.description, namespace: 'default', componentType: expectedEntities[0]!.spec!.type, + kind: expectedEntities[0]!.kind, + type: expectedEntities[0]!.spec!.type, lifecycle: expectedEntities[0]!.spec!.lifecycle, owner: expectedEntities[0]!.spec!.owner, authorization: { resourceRef: 'component:default/test-entity', }, }); - expect(documents[1]).toMatchObject({ + expect(documents[1]).toEqual({ title: expectedEntities[1].metadata.title, location: '/catalog/default/component/test-entity-2', text: expectedEntities[1].metadata.description, namespace: 'default', componentType: expectedEntities[1]!.spec!.type, + kind: expectedEntities[1]!.kind, + type: expectedEntities[1]!.spec!.type, lifecycle: expectedEntities[1]!.spec!.lifecycle, owner: expectedEntities[1]!.spec!.owner, authorization: { @@ -150,6 +149,61 @@ describe('DefaultCatalogCollatorFactory', () => { }); }); + it('maps a returned entity to an expected CatalogEntityDocument with custom transformer', async () => { + const customFactory = DefaultCatalogCollatorFactory.fromConfig(config, { + ...options, + entityTransformer: entity => ({ + title: `custom-title-${ + entity.metadata.title ?? entity.metadata.name + }`, + namespace: 'custom/namespace', + text: 'custom-text', + type: 'custom-type', + componentType: 'custom-component-type', + kind: 'custom-kind', + lifecycle: 'custom-lifecycle', + owner: 'custom-owner', + authorization: { + resourceRef: 'custom:resource/ref', + }, + location: '/custom/location', + }), + }); + const customCollator = await customFactory.getCollator(); + + const pipeline = TestPipeline.fromCollator(customCollator); + const { documents } = await pipeline.execute(); + + expect(documents[0]).toEqual({ + title: 'custom-title-test-entity', + location: '/catalog/default/component/test-entity', + text: 'custom-text', + namespace: 'custom/namespace', + componentType: 'custom-component-type', + kind: 'custom-kind', + type: 'custom-type', + lifecycle: 'custom-lifecycle', + owner: 'custom-owner', + authorization: { + resourceRef: 'component:default/test-entity', + }, + }); + expect(documents[1]).toEqual({ + title: 'custom-title-Test Entity', + location: '/catalog/default/component/test-entity-2', + text: 'custom-text', + namespace: 'custom/namespace', + componentType: 'custom-component-type', + kind: 'custom-kind', + type: 'custom-type', + lifecycle: 'custom-lifecycle', + owner: 'custom-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({}), { diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts index cf082c271c..ed81d39bbb 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Config } from '@backstage/config'; import { PluginEndpointDiscovery, TokenManager, @@ -23,16 +24,14 @@ import { CatalogClient, GetEntitiesRequest, } from '@backstage/catalog-client'; -import { stringifyEntityRef } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; -import { - catalogEntityReadPermission, - CatalogEntityDocument, -} from '@backstage/plugin-catalog-common'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha'; +import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; import { Permission } from '@backstage/plugin-permission-common'; import { Readable } from 'stream'; -import { getDocumentText } from './util'; +import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; +import { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer'; +import { stringifyEntityRef } from '@backstage/catalog-model'; /** @public */ export type DefaultCatalogCollatorFactoryOptions = { @@ -42,11 +41,12 @@ export type DefaultCatalogCollatorFactoryOptions = { filter?: GetEntitiesRequest['filter']; batchSize?: number; catalogClient?: CatalogApi; + entityTransformer?: CatalogCollatorEntityTransformer; }; /** @public */ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { - public readonly type: string = 'software-catalog'; + public readonly type = 'software-catalog'; public readonly visibilityPermission: Permission = catalogEntityReadPermission; @@ -55,6 +55,7 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { private batchSize: number; private readonly catalogClient: CatalogApi; private tokenManager: TokenManager; + private entityTransformer: CatalogCollatorEntityTransformer; static fromConfig( _config: Config, @@ -71,6 +72,7 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { filter, catalogClient, tokenManager, + entityTransformer, } = options; this.locationTemplate = @@ -80,23 +82,14 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { this.catalogClient = catalogClient || new CatalogClient({ discoveryApi: discovery }); this.tokenManager = tokenManager; + this.entityTransformer = + entityTransformer ?? defaultCatalogCollatorEntityTransformer; } async getCollator(): Promise<Readable> { return Readable.from(this.execute()); } - private applyArgsToFormat( - format: string, - args: Record<string, string>, - ): string { - let formatted = format; - for (const [key, value] of Object.entries(args)) { - formatted = formatted.replace(`:${key}`, value); - } - return formatted.toLowerCase(); - } - private async *execute(): AsyncGenerator<CatalogEntityDocument> { const { token } = await this.tokenManager.getToken(); let entitiesRetrieved = 0; @@ -123,24 +116,30 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { for (const entity of entities) { yield { - title: entity.metadata.title ?? entity.metadata.name, + ...this.entityTransformer(entity), + authorization: { + resourceRef: stringifyEntityRef(entity), + }, location: this.applyArgsToFormat(this.locationTemplate, { namespace: entity.metadata.namespace || 'default', kind: entity.kind, name: entity.metadata.name, }), - text: getDocumentText(entity), - componentType: entity.spec?.type?.toString() || 'other', - type: 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), - }, }; } } } + + private applyArgsToFormat( + format: string, + args: Record<string, string>, + ): string { + let formatted = format; + + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + + return formatted.toLowerCase(); + } } diff --git a/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.test.ts b/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.test.ts new file mode 100644 index 0000000000..8206a58599 --- /dev/null +++ b/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.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 { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer'; + +const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity', + name: 'test-entity', + description: 'The expected description', + namespace: 'namespace', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, +}; + +const userEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'test-user-entity', + description: 'The expected user description', + }, + spec: { + profile: { + displayName: 'User 1', + }, + }, +}; + +describe('DefaultCatalogCollatorEntityTransformer', () => { + describe('transform', () => { + it('maps a returned entity', async () => { + const document = defaultCatalogCollatorEntityTransformer(entity); + + expect(document).toMatchObject({ + title: entity.metadata.title, + text: entity.metadata.description, + namespace: entity.metadata.namespace, + componentType: entity.spec.type, + lifecycle: entity.spec.lifecycle, + owner: entity.spec.owner, + }); + }); + + it('maps a returned entity with default fallback', async () => { + const entityWithoutTitle = { + ...entity, + metadata: { + ...entity.metadata, + title: undefined, + namespace: undefined, + }, + spec: { + type: undefined, + lifecycle: undefined, + owner: undefined, + }, + }; + + const document = + defaultCatalogCollatorEntityTransformer(entityWithoutTitle); + + expect(document).toMatchObject({ + title: entity.metadata.name, + text: entity.metadata.description, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + }); + }); + + it('maps a returned user entity', async () => { + const document = defaultCatalogCollatorEntityTransformer(userEntity); + + expect(document).toMatchObject({ + title: userEntity.metadata.name, + text: `${userEntity.metadata.description} : ${userEntity.spec.profile.displayName}`, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + }); + }); + + it('maps a returned user entity without display name', async () => { + const testEntity = { + ...userEntity, + spec: undefined, + }; + + const document = defaultCatalogCollatorEntityTransformer(testEntity); + + expect(document).toMatchObject({ + title: userEntity.metadata.name, + text: userEntity.metadata.description, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + }); + }); + + it('maps a returned group entity', async () => { + const groupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'test-group-entity', + description: 'The expected group description', + }, + spec: { + profile: { + displayName: 'Group 1', + }, + }, + }; + + const document = defaultCatalogCollatorEntityTransformer(groupEntity); + + expect(document).toMatchObject({ + title: groupEntity.metadata.name, + text: `${groupEntity.metadata.description} : ${groupEntity.spec.profile.displayName}`, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.ts b/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.ts new file mode 100644 index 0000000000..2fc0c5d50d --- /dev/null +++ b/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, isGroupEntity, isUserEntity } from '@backstage/catalog-model'; +import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; + +const getDocumentText = (entity: Entity): string => { + const documentTexts: string[] = []; + documentTexts.push(entity.metadata.description || ''); + + if (isUserEntity(entity) || isGroupEntity(entity)) { + if (entity.spec?.profile?.displayName) { + documentTexts.push(entity.spec.profile.displayName); + } + } + + return documentTexts.join(' : '); +}; + +/** @public */ +export const defaultCatalogCollatorEntityTransformer: CatalogCollatorEntityTransformer = + (entity: Entity) => { + return { + title: entity.metadata.title ?? entity.metadata.name, + text: getDocumentText(entity), + componentType: entity.spec?.type?.toString() || 'other', + type: 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) || '', + }; + }; diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index 2602f0fada..3603ab2e60 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -16,6 +16,8 @@ export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; +export type { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; +export { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer'; /** * todo(backstage/techdocs-core): stop exporting this in a future release. diff --git a/plugins/catalog-backend/src/search/util.test.ts b/plugins/catalog-backend/src/search/util.test.ts deleted file mode 100644 index d2d1776338..0000000000 --- a/plugins/catalog-backend/src/search/util.test.ts +++ /dev/null @@ -1,146 +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 { - ComponentEntity, - GroupEntity, - UserEntity, -} from '@backstage/catalog-model'; -import { getDocumentText } from './util'; - -describe('getDocumentText', () => { - describe('kind is not User or Group', () => { - test('contains description if set', () => { - const entity = createComponent(); - entity.metadata.description = 'The expected description'; - const actual = getDocumentText(entity); - expect(actual).toContain(entity.metadata.description); - }); - - test('is empty if description is not set', () => { - const entity = createComponent(); - const actual = getDocumentText(entity); - expect(actual).toEqual(''); - }); - }); - - describe('kind is User', () => { - test('contains display name if set', () => { - const entity = createUser(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - }); - - test('contains description if set', () => { - const entity = createUser(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.metadata.description); - }); - - test('contains both description and display name if both are set', () => { - const entity = createUser(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - expect(actual).toContain(entity.metadata.description); - }); - - test('is empty if description and display name are not set', () => { - const entity = createUser(); - delete entity.metadata.description; - delete entity.spec.profile?.displayName; - const actual = getDocumentText(entity); - expect(actual).toEqual(''); - }); - }); - - describe('kind is Group', () => { - test('contains display name if set', () => { - const entity = createGroup(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - }); - - test('contains description if set', () => { - const entity = createGroup(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.metadata.description); - }); - - test('contains both description and display name if both are set', () => { - const entity = createGroup(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - expect(actual).toContain(entity.metadata.description); - }); - - test('is empty if description and display name are not set', () => { - const entity = createGroup(); - delete entity.metadata.description; - delete entity.spec.profile?.displayName; - const actual = getDocumentText(entity); - expect(actual).toEqual(''); - }); - }); -}); - -function createGroup(): GroupEntity { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: 'group-1', - description: 'The expected description', - }, - spec: { - type: 'team', - profile: { - displayName: 'Group 1', - }, - children: [], - }, - }; -} - -function createUser(): UserEntity { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: 'user-1', - description: 'The expected description', - }, - spec: { - profile: { - displayName: 'User 1', - }, - }, - }; -} - -function createComponent(): ComponentEntity { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'component-1', - }, - spec: { - lifecycle: 'experimental', - owner: 'someone', - type: 'service', - }, - }; -} diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts index 829e41c21e..2ee47dae19 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts @@ -20,6 +20,8 @@ import { createConditionTransformer } from '@backstage/plugin-permission-node'; import { isEntityKind } from '../permissions/rules/isEntityKind'; import { CatalogPermissionRule } from '../permissions/rules'; import { AuthorizedEntitiesCatalog } from './AuthorizedEntitiesCatalog'; +import { Cursor, EntityFilter, QueryEntitiesResponse } from '../catalog/types'; +import { Entity } from '@backstage/catalog-model'; describe('AuthorizedEntitiesCatalog', () => { const fakeCatalog = { @@ -30,6 +32,7 @@ describe('AuthorizedEntitiesCatalog', () => { facets: jest.fn(), refresh: jest.fn(), listAncestors: jest.fn(), + queryEntities: jest.fn(), }; const fakePermissionApi = { authorize: jest.fn(), @@ -156,6 +159,153 @@ describe('AuthorizedEntitiesCatalog', () => { }); }); + describe('queryEntities', () => { + it('returns empty response on DENY', async () => { + fakePermissionApi.authorizeConditional.mockResolvedValue([ + { result: AuthorizeResult.DENY }, + ]); + const catalog = createCatalog(); + + await expect( + catalog.queryEntities({ + authorizationToken: 'abcd', + filter: { key: 'kind', values: ['b'] }, + }), + ).resolves.toEqual({ + items: [], + pageInfo: {}, + totalItems: 0, + }); + + expect(fakeCatalog.queryEntities).not.toHaveBeenCalled(); + }); + + it('calls underlying catalog method on ALLOW', async () => { + fakePermissionApi.authorizeConditional.mockResolvedValue([ + { result: AuthorizeResult.ALLOW }, + ]); + const catalog = createCatalog(); + + await catalog.queryEntities({ + authorizationToken: 'abcd', + filter: { key: 'kind', values: ['b'] }, + }); + + expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ + authorizationToken: 'abcd', + filter: { key: 'kind', values: ['b'] }, + }); + }); + + it('calls underlying catalog method with correct filter on CONDITIONAL', async () => { + fakePermissionApi.authorizeConditional.mockResolvedValue([ + { + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_KIND', + params: { kinds: ['b'] }, + }, + }, + ]); + + const requestFilter: EntityFilter = { key: 'name', values: ['name'] }; + + const entities = [ + { + kind: 'component', + namespace: 'default', + name: 'a', + } as unknown as Entity, + { + kind: 'component', + namespace: 'default', + name: 'b1', + } as unknown as Entity, + ]; + + fakeCatalog.queryEntities.mockResolvedValue({ + items: entities, + pageInfo: { + nextCursor: { + isPrevious: false, + orderFieldValues: ['xxx', null], + filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, + }, + prevCursor: { + isPrevious: true, + orderFieldValues: ['a', null], + filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, + }, + }, + totalItems: 4, + } as QueryEntitiesResponse); + const catalog = createCatalog(isEntityKind); + + let response = await catalog.queryEntities({ + authorizationToken: 'abcd', + filter: { key: 'name', values: ['name'] }, + }); + + expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ + authorizationToken: 'abcd', + filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, + }); + + expect(response).toEqual({ + items: entities, + totalItems: 4, + pageInfo: { + nextCursor: { + isPrevious: false, + filter: requestFilter, + orderFieldValues: ['xxx', null], + }, + prevCursor: { + isPrevious: true, + filter: requestFilter, + orderFieldValues: ['a', null], + }, + }, + }); + + const cursor: Cursor = { + filter: requestFilter, + orderFields: [{ field: 'name', order: 'asc' }], + isPrevious: false, + orderFieldValues: ['a', null], + }; + response = await catalog.queryEntities({ + authorizationToken: 'abcd', + cursor, + }); + + expect(fakeCatalog.queryEntities).toHaveBeenNthCalledWith(2, { + authorizationToken: 'abcd', + cursor: { + ...cursor, + filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, + }, + }); + + expect(response).toEqual({ + items: entities, + totalItems: 4, + pageInfo: { + nextCursor: { + isPrevious: false, + filter: requestFilter, + orderFieldValues: ['xxx', null], + }, + prevCursor: { + isPrevious: true, + filter: requestFilter, + orderFieldValues: ['a', null], + }, + }, + }); + }); + }); + describe('removeEntityByUid', () => { it('throws error on DENY', async () => { fakeCatalog.entities.mockResolvedValue({ diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts index c62e9f9a84..0ce1ee87e7 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts @@ -18,7 +18,7 @@ import { NotAllowedError } from '@backstage/errors'; import { catalogEntityDeletePermission, catalogEntityReadPermission, -} from '@backstage/plugin-catalog-common'; +} from '@backstage/plugin-catalog-common/alpha'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { AuthorizeResult, @@ -26,6 +26,7 @@ import { } from '@backstage/plugin-permission-common'; import { ConditionTransformer } from '@backstage/plugin-permission-node'; import { + Cursor, EntitiesBatchRequest, EntitiesBatchResponse, EntitiesCatalog, @@ -35,8 +36,11 @@ import { EntityFacetsRequest, EntityFacetsResponse, EntityFilter, + QueryEntitiesRequest, + QueryEntitiesResponse, } from '../catalog/types'; import { basicEntityFilter } from './request/basicEntityFilter'; +import { isQueryEntitiesCursorRequest } from './util'; export class AuthorizedEntitiesCatalog implements EntitiesCatalog { constructor( @@ -106,6 +110,80 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { return this.entitiesCatalog.entitiesBatch(request); } + async queryEntities( + request: QueryEntitiesRequest, + ): Promise<QueryEntitiesResponse> { + const authorizeDecision = ( + await this.permissionApi.authorizeConditional( + [{ permission: catalogEntityReadPermission }], + { token: request.authorizationToken }, + ) + )[0]; + + if (authorizeDecision.result === AuthorizeResult.DENY) { + return { + items: [], + pageInfo: {}, + totalItems: 0, + }; + } + + if (authorizeDecision.result === AuthorizeResult.CONDITIONAL) { + const permissionFilter: EntityFilter = this.transformConditions( + authorizeDecision.conditions, + ); + + let permissionedRequest: QueryEntitiesRequest; + let requestFilter: EntityFilter | undefined; + + if (isQueryEntitiesCursorRequest(request)) { + requestFilter = request.cursor.filter; + + permissionedRequest = { + ...request, + cursor: { + ...request.cursor, + filter: request.cursor.filter + ? { allOf: [permissionFilter, request.cursor.filter] } + : permissionFilter, + }, + }; + } else { + permissionedRequest = { + ...request, + filter: request.filter + ? { allOf: [permissionFilter, request.filter] } + : permissionFilter, + }; + requestFilter = request.filter; + } + + const response = await this.entitiesCatalog.queryEntities( + permissionedRequest, + ); + + const prevCursor: Cursor | undefined = response.pageInfo.prevCursor && { + ...response.pageInfo.prevCursor, + filter: requestFilter, + }; + + const nextCursor: Cursor | undefined = response.pageInfo.nextCursor && { + ...response.pageInfo.nextCursor, + filter: requestFilter, + }; + + return { + ...response, + pageInfo: { + prevCursor, + nextCursor, + }, + }; + } + + return this.entitiesCatalog.queryEntities(request); + } + async removeEntityByUid( uid: string, options?: { authorizationToken?: string }, diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts index a73597649e..68bb1553a8 100644 --- a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts @@ -21,7 +21,7 @@ import { catalogLocationCreatePermission, catalogLocationDeletePermission, catalogLocationReadPermission, -} from '@backstage/plugin-catalog-common'; +} from '@backstage/plugin-catalog-common/alpha'; import { AuthorizeResult, PermissionEvaluator, diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts index 8634fbf86d..cbc728750a 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts @@ -15,7 +15,7 @@ */ import { NotAllowedError } from '@backstage/errors'; -import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common'; +import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha'; import { AuthorizeResult, PermissionEvaluator, diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 48bf6a08ef..7da5d69767 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -76,10 +76,10 @@ import { DefaultCatalogRulesEnforcer } from '../ingestion/CatalogRules'; import { Config } from '@backstage/config'; import { Logger } from 'winston'; import { connectEntityProviders } from '../processing/connectEntityProviders'; -import { - CatalogPermissionRule, - permissionRules as catalogPermissionRules, -} from '../permissions/rules'; +import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { EntitiesSearchFilter } from '../catalog/types'; +import { permissionRules as catalogPermissionRules } from '../permissions/rules'; +import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionAuthorizer, PermissionEvaluator, @@ -94,11 +94,20 @@ import { basicEntityFilter } from './request/basicEntityFilter'; import { catalogPermissions, RESOURCE_TYPE_CATALOG_ENTITY, -} from '@backstage/plugin-catalog-common'; +} from '@backstage/plugin-catalog-common/alpha'; import { AuthorizedLocationService } from './AuthorizedLocationService'; import { DefaultProviderDatabase } from '../database/DefaultProviderDatabase'; import { DefaultCatalogDatabase } from '../database/DefaultCatalogDatabase'; +/** + * This is a duplicate of the alpha `CatalogPermissionRule` type, for use in the stable API. + * + * @public + */ +export type CatalogPermissionRuleInput< + TParams extends PermissionRuleParams = PermissionRuleParams, +> = PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>; + /** @public */ export type CatalogEnvironment = { logger: Logger; @@ -154,7 +163,7 @@ export class CatalogBuilder { maxSeconds: 150, }); private locationAnalyzer: LocationAnalyzer | undefined = undefined; - private readonly permissionRules: CatalogPermissionRule[]; + private readonly permissionRules: CatalogPermissionRuleInput[]; private allowedLocationType: string[]; private legacySingleProcessorValidation = false; @@ -379,11 +388,10 @@ export class CatalogBuilder { * {@link @backstage/plugin-permission-node#PermissionRule}. * * @param permissionRules - Additional permission rules - * @alpha */ addPermissionRules( ...permissionRules: Array< - CatalogPermissionRule | Array<CatalogPermissionRule> + CatalogPermissionRuleInput | Array<CatalogPermissionRuleInput> > ) { this.permissionRules.push(...permissionRules.flat()); @@ -453,10 +461,11 @@ export class CatalogBuilder { legacySingleProcessorValidation: this.legacySingleProcessorValidation, }); const stitcher = new Stitcher(dbClient, logger); - const unauthorizedEntitiesCatalog = new DefaultEntitiesCatalog( - dbClient, + const unauthorizedEntitiesCatalog = new DefaultEntitiesCatalog({ + database: dbClient, + logger, stitcher, - ); + }); let permissionEvaluator: PermissionEvaluator; if ('authorizeConditional' in permissions) { diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index effed7f8f1..a6b164001c 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -19,9 +19,11 @@ import { } from '@backstage/backend-plugin-api'; import { CatalogBuilder } from './CatalogBuilder'; import { - CatalogProcessor, CatalogProcessingExtensionPoint, catalogProcessingExtensionPoint, +} from '@backstage/plugin-catalog-node/alpha'; +import { + CatalogProcessor, EntityProvider, } from '@backstage/plugin-catalog-node'; import { loggerToWinstonLogger } from '@backstage/backend-common'; @@ -56,7 +58,7 @@ class CatalogExtensionPointImpl implements CatalogProcessingExtensionPoint { * @alpha */ export const catalogPlugin = createBackendPlugin({ - id: 'catalog', + pluginId: 'catalog', register(env) { const processingExtensions = new CatalogExtensionPointImpl(); // plugins depending on this API will be initialized before this plugins init method is executed. @@ -97,11 +99,7 @@ export const catalogPlugin = createBackendPlugin({ const { processingEngine, router } = await builder.build(); await processingEngine.start(); - lifecycle.addShutdownHook({ - fn: async () => { - await processingEngine.stop(); - }, - }); + lifecycle.addShutdownHook(() => processingEngine.stop()); httpRouter.use(router); }, }); diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 6735709662..275bd1c42c 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -14,10 +14,15 @@ * limitations under the License. */ +import { getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { Knex } from 'knex'; -import { v4 as uuid } from 'uuid'; +import { v4 as uuid, v4 } from 'uuid'; +import { + QueryEntitiesCursorRequest, + QueryEntitiesInitialRequest, +} from '../catalog/types'; import { applyDatabaseMigrations } from '../database/migrations'; import { DbFinalEntitiesRow, @@ -31,6 +36,12 @@ import { DefaultEntitiesCatalog } from './DefaultEntitiesCatalog'; import { EntitiesRequest } from '../catalog/types'; describe('DefaultEntitiesCatalog', () => { + let knex: Knex; + + afterEach(async () => { + await knex.destroy(); + }); + const databases = TestDatabases.create({ ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); @@ -38,13 +49,11 @@ describe('DefaultEntitiesCatalog', () => { const stitcher: Stitcher = { stitch } as any; async function createDatabase(databaseId: TestDatabaseId) { - const knex = await databases.init(databaseId); + knex = await databases.init(databaseId); await applyDatabaseMigrations(knex); - return { knex }; } async function addEntity( - knex: Knex, entity: Entity, parents: { source?: string; entity?: Entity }[], ) { @@ -78,11 +87,14 @@ describe('DefaultEntitiesCatalog', () => { }); } + const search = await buildEntitySearch(id, entity); + await knex<DbSearchRow>('search').insert(search); + return id; } - async function addEntityToSearch(knex: Knex, entity: Entity) { - const id = uuid(); + async function addEntityToSearch(entity: Entity) { + const id = entity.metadata.uid || v4(); const entityRef = stringifyEntityRef(entity); const entityJson = JSON.stringify(entity); @@ -120,7 +132,7 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'should return the ancestry with one parent, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); const grandparent: Entity = { apiVersion: 'a', @@ -141,11 +153,15 @@ describe('DefaultEntitiesCatalog', () => { spec: {}, }; - await addEntity(knex, grandparent, [{ source: 's' }]); - await addEntity(knex, parent, [{ entity: grandparent }]); - await addEntity(knex, root, [{ entity: parent }]); + await addEntity(grandparent, [{ source: 's' }]); + await addEntity(parent, [{ entity: grandparent }]); + await addEntity(root, [{ entity: parent }]); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); const result = await catalog.entityAncestry('k:default/root'); expect(result.rootEntityRef).toEqual('k:default/root'); @@ -174,8 +190,12 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'should throw error if the entity does not exist, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + await createDatabase(databaseId); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); await expect(() => catalog.entityAncestry('k:default/root'), ).rejects.toThrow('No such entity k:default/root'); @@ -186,7 +206,7 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'should return the ancestry with multiple parents, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); const grandparent: Entity = { apiVersion: 'a', @@ -213,12 +233,16 @@ describe('DefaultEntitiesCatalog', () => { spec: {}, }; - await addEntity(knex, grandparent, [{ source: 's' }]); - await addEntity(knex, parent1, [{ entity: grandparent }]); - await addEntity(knex, parent2, [{ entity: grandparent }]); - await addEntity(knex, root, [{ entity: parent1 }, { entity: parent2 }]); + await addEntity(grandparent, [{ source: 's' }]); + await addEntity(parent1, [{ entity: grandparent }]); + await addEntity(parent2, [{ entity: grandparent }]); + await addEntity(root, [{ entity: parent1 }, { entity: parent2 }]); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); const result = await catalog.entityAncestry('k:default/root'); expect(result.rootEntityRef).toEqual('k:default/root'); @@ -257,7 +281,7 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'should return correct entity for simple filter, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); const entity1: Entity = { apiVersion: 'a', kind: 'k', @@ -272,9 +296,13 @@ describe('DefaultEntitiesCatalog', () => { test: 'test value', }, }; - await addEntityToSearch(knex, entity1); - await addEntityToSearch(knex, entity2); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + await addEntityToSearch(entity1); + await addEntityToSearch(entity2); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); const testFilter = { key: 'spec.test', @@ -291,7 +319,7 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'should return correct entity for negation filter, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); const entity1: Entity = { apiVersion: 'a', kind: 'k', @@ -306,9 +334,13 @@ describe('DefaultEntitiesCatalog', () => { test: 'test value', }, }; - await addEntityToSearch(knex, entity1); - await addEntityToSearch(knex, entity2); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + await addEntityToSearch(entity1); + await addEntityToSearch(entity2); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); const testFilter = { not: { @@ -327,7 +359,7 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'should return correct entities for nested filter, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); const entity1: Entity = { apiVersion: 'a', kind: 'k', @@ -352,11 +384,15 @@ describe('DefaultEntitiesCatalog', () => { metadata: { name: 'four', org: 'b', color: 'blue' }, spec: {}, }; - await addEntityToSearch(knex, entity1); - await addEntityToSearch(knex, entity2); - await addEntityToSearch(knex, entity3); - await addEntityToSearch(knex, entity4); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + await addEntityToSearch(entity1); + await addEntityToSearch(entity2); + await addEntityToSearch(entity3); + await addEntityToSearch(entity4); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); const testFilter1 = { key: 'metadata.org', @@ -397,7 +433,7 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'should return correct entities for complex negation filter, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); const entity1: Entity = { apiVersion: 'a', kind: 'k', @@ -410,9 +446,13 @@ describe('DefaultEntitiesCatalog', () => { metadata: { name: 'two', org: 'b', desc: 'description' }, spec: {}, }; - await addEntityToSearch(knex, entity1); - await addEntityToSearch(knex, entity2); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + await addEntityToSearch(entity1); + await addEntityToSearch(entity2); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); const testFilter1 = { key: 'metadata.org', @@ -440,7 +480,7 @@ describe('DefaultEntitiesCatalog', () => { 'should return no matches for an empty values array, %p', // NOTE: An empty values array is not a sensible input in a realistic scenario. async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); const entity1: Entity = { apiVersion: 'a', kind: 'k', @@ -453,9 +493,13 @@ describe('DefaultEntitiesCatalog', () => { metadata: { name: 'two' }, spec: {}, }; - await addEntityToSearch(knex, entity1); - await addEntityToSearch(knex, entity2); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + await addEntityToSearch(entity1); + await addEntityToSearch(entity2); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); const testFilter = { key: 'kind', @@ -472,9 +516,8 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'should return both target and targetRef for entities', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); await addEntity( - knex, { apiVersion: 'a', kind: 'k', @@ -485,7 +528,6 @@ describe('DefaultEntitiesCatalog', () => { [], ); await addEntity( - knex, { apiVersion: 'a', kind: 'k', @@ -500,7 +542,11 @@ describe('DefaultEntitiesCatalog', () => { }, [], ); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); const { entities } = await catalog.entities(); @@ -529,7 +575,7 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'can order and combine with filtering, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); const entity1: Entity = { apiVersion: 'a', @@ -555,12 +601,16 @@ describe('DefaultEntitiesCatalog', () => { metadata: { name: 'n4' }, spec: { a: 'baz', b: 'only' }, }; - await addEntityToSearch(knex, entity1); - await addEntityToSearch(knex, entity2); - await addEntityToSearch(knex, entity3); - await addEntityToSearch(knex, entity4); + await addEntityToSearch(entity1); + await addEntityToSearch(entity2); + await addEntityToSearch(entity3); + await addEntityToSearch(entity4); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); function f(request: EntitiesRequest): Promise<string[]> { return catalog @@ -623,10 +673,9 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'queries for entities by ref, including duplicates, and gracefully returns null for missing entities, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); await addEntity( - knex, { apiVersion: 'a', kind: 'k', @@ -637,7 +686,6 @@ describe('DefaultEntitiesCatalog', () => { [], ); await addEntity( - knex, { apiVersion: 'a', kind: 'k', @@ -648,7 +696,11 @@ describe('DefaultEntitiesCatalog', () => { [], ); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); const { items } = await catalog.entitiesBatch({ entityRefs: [ @@ -672,13 +724,628 @@ describe('DefaultEntitiesCatalog', () => { }, 60_000, ); + + it.each(databases.eachSupportedId())( + 'queries for entities by ref, including filtering, %p', + async databaseId => { + await createDatabase(databaseId); + + await addEntity( + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'one' }, + spec: {}, + relations: [], + }, + [], + ); + await addEntity( + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'two' }, + spec: { owner: 'me' }, + relations: [], + }, + [], + ); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const { items } = await catalog.entitiesBatch({ + entityRefs: ['k:default/two', 'k:default/one'], + filter: { key: 'spec.owner', values: ['me'] }, + }); + + expect(items.map(e => e && stringifyEntityRef(e))).toEqual([ + 'k:default/two', + null, + ]); + }, + 60_000, + ); + }); + + describe('queryEntities', () => { + it.each(databases.eachSupportedId())( + 'should return paginated entities and scroll the items accordingly, %p', + async databaseId => { + await createDatabase(databaseId); + + const names = ['B', 'F', 'A', 'G', 'D', 'C', 'E']; + const entities: Entity[] = names.map(name => entityFrom(name)); + + const notFoundEntities: Entity[] = [ + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'something' }, + spec: {}, + }, + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'something else' }, + spec: {}, + }, + ]; + + await Promise.all( + entities.concat(notFoundEntities).map(e => addEntityToSearch(e)), + ); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const filter = { + key: 'spec.should_include_this', + }; + + const limit = 2; + + // initial request + const request1: QueryEntitiesInitialRequest = { + filter, + limit, + orderFields: [{ field: 'metadata.name', order: 'asc' }], + }; + const response1 = await catalog.queryEntities(request1); + expect(response1.items).toEqual([entityFrom('A'), entityFrom('B')]); + expect(response1.pageInfo.nextCursor).toBeDefined(); + expect(response1.pageInfo.prevCursor).toBeUndefined(); + expect(response1.totalItems).toBe(names.length); + + // second request (forward) + const request2: QueryEntitiesCursorRequest = { + cursor: response1.pageInfo.nextCursor!, + limit, + }; + const response2 = await catalog.queryEntities(request2); + expect(response2.items).toEqual([entityFrom('C'), entityFrom('D')]); + expect(response2.pageInfo.nextCursor).toBeDefined(); + expect(response2.pageInfo.prevCursor).toBeDefined(); + expect(response2.totalItems).toBe(names.length); + + // third request (forward) + const request3: QueryEntitiesCursorRequest = { + cursor: response2.pageInfo.nextCursor!, + limit, + }; + const response3 = await catalog.queryEntities(request3); + expect(response3.items).toEqual([entityFrom('E'), entityFrom('F')]); + expect(response3.pageInfo.nextCursor).toBeDefined(); + expect(response3.pageInfo.prevCursor).toBeDefined(); + expect(response3.totalItems).toBe(names.length); + + // fourth request (backwards) + const request4: QueryEntitiesCursorRequest = { + cursor: response3.pageInfo.prevCursor!, + limit, + }; + const response4 = await catalog.queryEntities(request4); + expect(response4.items).toEqual([entityFrom('C'), entityFrom('D')]); + expect(response4.pageInfo.nextCursor).toBeDefined(); + expect(response4.pageInfo.prevCursor).toBeDefined(); + expect(response4.totalItems).toBe(names.length); + + // fifth request (backwards) + const request5: QueryEntitiesCursorRequest = { + cursor: response4.pageInfo.prevCursor!, + limit, + }; + const response5 = await catalog.queryEntities(request5); + expect(response5.items).toEqual([entityFrom('A'), entityFrom('B')]); + expect(response5.pageInfo.nextCursor).toBeDefined(); + expect(response5.pageInfo.prevCursor).toBeUndefined(); + expect(response5.totalItems).toBe(names.length); + + // sixth request (forward) + const request6: QueryEntitiesCursorRequest = { + cursor: response5.pageInfo.nextCursor!, + limit, + }; + const response6 = await catalog.queryEntities(request6); + expect(response6.items).toEqual([entityFrom('C'), entityFrom('D')]); + expect(response6.pageInfo.nextCursor).toBeDefined(); + expect(response6.pageInfo.prevCursor).toBeDefined(); + expect(response6.totalItems).toBe(names.length); + + // seventh request (forward) + const request7: QueryEntitiesCursorRequest = { + cursor: response6.pageInfo.nextCursor!, + limit, + }; + const response7 = await catalog.queryEntities(request7); + expect(response7.items).toEqual([entityFrom('E'), entityFrom('F')]); + expect(response7.pageInfo.nextCursor).toBeDefined(); + expect(response7.pageInfo.prevCursor).toBeDefined(); + expect(response7.totalItems).toBe(names.length); + + // seventh.2 request (forward with a different limit) + const request7bis: QueryEntitiesCursorRequest = { + cursor: response6.pageInfo.nextCursor!, + limit: limit + 1, + }; + const response7bis = await catalog.queryEntities(request7bis); + expect(response7bis.items).toEqual([ + entityFrom('E'), + entityFrom('F'), + entityFrom('G'), + ]); + expect(response7bis.pageInfo.nextCursor).toBeUndefined(); + expect(response7bis.pageInfo.prevCursor).toBeDefined(); + expect(response7bis.totalItems).toBe(names.length); + + // last request (forward) + const request8: QueryEntitiesCursorRequest = { + cursor: response7.pageInfo.nextCursor!, + limit, + }; + const response8 = await catalog.queryEntities(request8); + expect(response8.items).toEqual([entityFrom('G')]); + expect(response8.pageInfo.nextCursor).toBeUndefined(); + expect(response8.pageInfo.prevCursor).toBeDefined(); + expect(response8.totalItems).toBe(names.length); + }, + ); + + it.each(databases.eachSupportedId())( + 'should return paginated entities ordered in descending order and scroll the items accordingly, %p', + async databaseId => { + await createDatabase(databaseId); + + const names = ['B', 'F', 'A', 'G', 'D', 'C', 'E']; + const entities: Entity[] = names.map(name => entityFrom(name)); + + const notFoundEntities: Entity[] = [ + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'something' }, + spec: {}, + }, + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'something else' }, + spec: {}, + }, + ]; + + await Promise.all( + entities.concat(notFoundEntities).map(e => addEntityToSearch(e)), + ); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const filter = { + key: 'spec.should_include_this', + }; + + const limit = 2; + + // initial request + const request1: QueryEntitiesInitialRequest = { + filter, + limit, + orderFields: [{ field: 'metadata.name', order: 'desc' }], + }; + const response1 = await catalog.queryEntities(request1); + expect(response1.items).toEqual([entityFrom('G'), entityFrom('F')]); + expect(response1.pageInfo.nextCursor).toBeDefined(); + expect(response1.pageInfo.prevCursor).toBeUndefined(); + expect(response1.totalItems).toBe(names.length); + + // second request (forward) + const request2: QueryEntitiesCursorRequest = { + cursor: response1.pageInfo.nextCursor!, + limit, + }; + const response2 = await catalog.queryEntities(request2); + expect(response2.items).toEqual([entityFrom('E'), entityFrom('D')]); + expect(response2.pageInfo.nextCursor).toBeDefined(); + expect(response2.pageInfo.prevCursor).toBeDefined(); + expect(response2.totalItems).toBe(names.length); + + // third request (forward) + const request3: QueryEntitiesCursorRequest = { + cursor: response2.pageInfo.nextCursor!, + limit, + }; + const response3 = await catalog.queryEntities(request3); + expect(response3.items).toEqual([entityFrom('C'), entityFrom('B')]); + expect(response3.pageInfo.nextCursor).toBeDefined(); + expect(response3.pageInfo.prevCursor).toBeDefined(); + expect(response3.totalItems).toBe(names.length); + + // fourth request (backwards) + const request4: QueryEntitiesCursorRequest = { + cursor: response3.pageInfo.prevCursor!, + limit, + }; + const response4 = await catalog.queryEntities(request4); + + expect(response4.items).toEqual([entityFrom('E'), entityFrom('D')]); + expect(response4.pageInfo.nextCursor).toBeDefined(); + expect(response4.pageInfo.prevCursor).toBeDefined(); + expect(response4.totalItems).toBe(names.length); + + // fifth request (backwards) + const request5: QueryEntitiesCursorRequest = { + cursor: response4.pageInfo.prevCursor!, + limit, + }; + const response5 = await catalog.queryEntities(request5); + expect(response5.items).toEqual([entityFrom('G'), entityFrom('F')]); + expect(response5.pageInfo.nextCursor).toBeDefined(); + expect(response5.pageInfo.prevCursor).toBeUndefined(); + expect(response5.totalItems).toBe(names.length); + + // sixth request (forward) + const request6: QueryEntitiesCursorRequest = { + cursor: response5.pageInfo.nextCursor!, + limit, + }; + const response6 = await catalog.queryEntities(request6); + expect(response6.items).toEqual([entityFrom('E'), entityFrom('D')]); + expect(response6.pageInfo.nextCursor).toBeDefined(); + expect(response6.pageInfo.prevCursor).toBeDefined(); + expect(response6.totalItems).toBe(names.length); + + // seventh request (forward) + const request7: QueryEntitiesCursorRequest = { + cursor: response6.pageInfo.nextCursor!, + limit, + }; + const response7 = await catalog.queryEntities(request7); + expect(response7.items).toEqual([entityFrom('C'), entityFrom('B')]); + expect(response7.pageInfo.nextCursor).toBeDefined(); + expect(response7.pageInfo.prevCursor).toBeDefined(); + expect(response7.totalItems).toBe(names.length); + + // seventh.2 request (forward with a different limit) + const request7bis: QueryEntitiesCursorRequest = { + cursor: response6.pageInfo.nextCursor!, + limit: limit + 1, + }; + const response7bis = await catalog.queryEntities(request7bis); + expect(response7bis.items).toEqual([ + entityFrom('C'), + entityFrom('B'), + entityFrom('A'), + ]); + expect(response7bis.pageInfo.nextCursor).toBeUndefined(); + expect(response7bis.pageInfo.prevCursor).toBeDefined(); + expect(response7bis.totalItems).toBe(names.length); + + // last request (forward) + const request8: QueryEntitiesCursorRequest = { + cursor: response7.pageInfo.nextCursor!, + limit, + }; + const response8 = await catalog.queryEntities(request8); + expect(response8.items).toEqual([entityFrom('A')]); + expect(response8.pageInfo.nextCursor).toBeUndefined(); + expect(response8.pageInfo.prevCursor).toBeDefined(); + expect(response8.totalItems).toBe(names.length); + }, + ); + + it.each(databases.eachSupportedId())( + 'should filter the results when query is provided, %p', + async databaseId => { + await createDatabase(databaseId); + + const names = ['lion', 'cat', 'atcatss', 'dog', 'dogcat', 'aa', 's']; + const entities: Entity[] = names.map(name => entityFrom(name)); + + const notFoundEntities: Entity[] = [ + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'something' }, + spec: {}, + }, + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'something else' }, + spec: {}, + }, + ]; + + await Promise.all( + entities.concat(notFoundEntities).map(e => addEntityToSearch(e)), + ); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const filter = { + key: 'spec.should_include_this', + }; + + const request: QueryEntitiesInitialRequest = { + filter, + limit: 100, + + orderFields: [{ field: 'metadata.name', order: 'asc' }], + fullTextFilter: { term: 'cAt ' }, + }; + const response = await catalog.queryEntities(request); + expect(response.items).toEqual([ + entityFrom('atcatss'), + entityFrom('cat'), + entityFrom('dogcat'), + ]); + expect(response.pageInfo.nextCursor).toBeUndefined(); + expect(response.pageInfo.prevCursor).toBeUndefined(); + expect(response.totalItems).toBe(3); + }, + ); + + it.each(databases.eachSupportedId())( + 'should include totalItems and empty entities in the response in case limit is zero, %p', + async databaseId => { + await createDatabase(databaseId); + + await Promise.all( + Array(20) + .fill(0) + .map(() => + addEntityToSearch({ + apiVersion: 'a', + kind: 'k', + metadata: { name: v4() }, + }), + ), + ); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const request: QueryEntitiesInitialRequest = { + limit: 0, + }; + const response = await catalog.queryEntities(request); + expect(response).toEqual({ totalItems: 20, items: [], pageInfo: {} }); + }, + ); + + it.each(databases.eachSupportedId())( + 'should paginate results accordingly in case of clashing items, %p', + async databaseId => { + await createDatabase(databaseId); + + await Promise.all([ + addEntityToSearch(entityFrom('AA')), + addEntityToSearch(entityFrom('AA', { namespace: 'namespace2' })), + addEntityToSearch(entityFrom('AA', { namespace: 'namespace3' })), + addEntityToSearch(entityFrom('AA', { namespace: 'namespace4' })), + addEntityToSearch(entityFrom('CC')), + addEntityToSearch(entityFrom('DD')), + ]); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const limit = 2; + + // initial request + const request1: QueryEntitiesInitialRequest = { + limit, + orderFields: [{ field: 'metadata.name', order: 'asc' }], + }; + const response1 = await catalog.queryEntities(request1); + expect(response1.items).toMatchObject([ + entityFrom('AA'), + entityFrom('AA'), + ]); + expect(response1.pageInfo.nextCursor).toBeDefined(); + expect(response1.pageInfo.prevCursor).toBeUndefined(); + expect(response1.totalItems).toBe(6); + + // second request (forward) + const request2: QueryEntitiesCursorRequest = { + cursor: response1.pageInfo.nextCursor!, + limit, + }; + const response2 = await catalog.queryEntities(request2); + expect(response2.items).toMatchObject([ + entityFrom('AA'), + entityFrom('AA'), + ]); + expect(response2.pageInfo.nextCursor).toBeDefined(); + expect(response2.pageInfo.prevCursor).toBeDefined(); + expect(response2.totalItems).toBe(6); + + // third request (forward) + const request3: QueryEntitiesCursorRequest = { + cursor: response2.pageInfo.nextCursor!, + limit, + }; + const response3 = await catalog.queryEntities(request3); + expect(response3.items).toEqual([entityFrom('CC'), entityFrom('DD')]); + expect(response3.pageInfo.nextCursor).toBeUndefined(); + expect(response3.pageInfo.prevCursor).toBeDefined(); + expect(response3.totalItems).toBe(6); + + // forth request (backward) + const request4: QueryEntitiesCursorRequest = { + cursor: response3.pageInfo.prevCursor!, + limit, + }; + const response4 = await catalog.queryEntities(request4); + expect(response4.items).toMatchObject([ + entityFrom('AA'), + entityFrom('AA'), + ]); + expect(response4.pageInfo.nextCursor).toBeDefined(); + expect(response4.pageInfo.prevCursor).toBeDefined(); + expect(response4.totalItems).toBe(6); + + // fifth request (backward) + const request5: QueryEntitiesCursorRequest = { + cursor: response4.pageInfo.prevCursor!, + limit, + }; + const response5 = await catalog.queryEntities(request5); + expect(response5.items).toMatchObject([ + entityFrom('AA'), + entityFrom('AA'), + ]); + expect(response5.pageInfo.nextCursor).toBeDefined(); + expect(response5.pageInfo.prevCursor).toBeUndefined(); + expect(response5.totalItems).toBe(6); + }, + ); + + it.each(databases.eachSupportedId())( + 'should paginate results without sort fields, %p', + async databaseId => { + await createDatabase(databaseId); + + await Promise.all([ + addEntityToSearch(entityFrom('AA', { uid: 'id1' })), + addEntityToSearch(entityFrom('CC', { uid: 'id2' })), + addEntityToSearch( + entityFrom('AA', { namespace: 'namespace2', uid: 'id4' }), + ), + addEntityToSearch( + entityFrom('AA', { namespace: 'namespace3', uid: 'id5' }), + ), + addEntityToSearch( + entityFrom('AA', { namespace: 'namespace4', uid: 'id6' }), + ), + addEntityToSearch(entityFrom('DD', { uid: 'id3' })), + ]); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const limit = 2; + + // initial request + const request1: QueryEntitiesInitialRequest = { + limit, + }; + const response1 = await catalog.queryEntities(request1); + expect(response1.items).toMatchObject([ + entityFrom('AA'), + entityFrom('CC'), + ]); + expect(response1.pageInfo.nextCursor).toBeDefined(); + expect(response1.pageInfo.prevCursor).toBeUndefined(); + expect(response1.totalItems).toBe(6); + + // second request (forward) + const request2: QueryEntitiesCursorRequest = { + cursor: response1.pageInfo.nextCursor!, + limit, + }; + const response2 = await catalog.queryEntities(request2); + expect(response2.items).toMatchObject([ + entityFrom('DD'), + entityFrom('AA', { namespace: 'namespace2' }), + ]); + expect(response2.pageInfo.nextCursor).toBeDefined(); + expect(response2.pageInfo.prevCursor).toBeDefined(); + expect(response2.totalItems).toBe(6); + + // third request (forward) + const request3: QueryEntitiesCursorRequest = { + cursor: response2.pageInfo.nextCursor!, + limit, + }; + const response3 = await catalog.queryEntities(request3); + expect(response3.items).toMatchObject([ + entityFrom('AA', { namespace: 'namespace3' }), + entityFrom('AA', { namespace: 'namespace4' }), + ]); + expect(response3.pageInfo.nextCursor).toBeUndefined(); + expect(response3.pageInfo.prevCursor).toBeDefined(); + expect(response3.totalItems).toBe(6); + + // forth request (backward) + const request4: QueryEntitiesCursorRequest = { + cursor: response3.pageInfo.prevCursor!, + limit, + }; + const response4 = await catalog.queryEntities(request4); + expect(response4.items).toMatchObject([ + entityFrom('DD'), + entityFrom('AA', { namespace: 'namespace2' }), + ]); + expect(response4.pageInfo.nextCursor).toBeDefined(); + expect(response4.pageInfo.prevCursor).toBeDefined(); + expect(response4.totalItems).toBe(6); + + // fifth request (backward) + const request5: QueryEntitiesCursorRequest = { + cursor: response4.pageInfo.prevCursor!, + limit, + }; + const response5 = await catalog.queryEntities(request5); + expect(response5.items).toMatchObject([ + entityFrom('AA'), + entityFrom('CC'), + ]); + expect(response5.pageInfo.nextCursor).toBeDefined(); + expect(response5.pageInfo.prevCursor).toBeUndefined(); + expect(response5.totalItems).toBe(6); + }, + ); }); describe('removeEntityByUid', () => { it.each(databases.eachSupportedId())( 'also clears parent hashes, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); const grandparent: Entity = { apiVersion: 'a', @@ -717,15 +1384,15 @@ describe('DefaultEntitiesCatalog', () => { spec: {}, }; - await addEntity(knex, grandparent, [{ source: 's' }]); - await addEntity(knex, parent1, [{ entity: grandparent }]); - await addEntity(knex, parent2, [{ entity: grandparent }]); - const uid = await addEntity(knex, root, [ + await addEntity(grandparent, [{ source: 's' }]); + await addEntity(parent1, [{ entity: grandparent }]); + await addEntity(parent2, [{ entity: grandparent }]); + const uid = await addEntity(root, [ { entity: parent1 }, { entity: parent2 }, ]); - await addEntity(knex, unrelated1, []); - await addEntity(knex, unrelated2, []); + await addEntity(unrelated1, []); + await addEntity(unrelated2, []); await knex('refresh_state').update({ result_hash: 'not-changed' }); await knex('relations').insert({ originating_entity_id: uid, @@ -740,7 +1407,11 @@ describe('DefaultEntitiesCatalog', () => { target_entity_ref: 'k:default/root', }); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); await catalog.removeEntityByUid(uid); await expect( @@ -767,27 +1438,31 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'can filter and collect properly, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); - await addEntityToSearch(knex, { + await addEntityToSearch({ apiVersion: 'a', kind: 'k', metadata: { name: 'one' }, spec: {}, }); - await addEntityToSearch(knex, { + await addEntityToSearch({ apiVersion: 'a', kind: 'k', metadata: { name: 'two' }, spec: {}, }); - await addEntityToSearch(knex, { + await addEntityToSearch({ apiVersion: 'a', kind: 'k2', metadata: { name: 'two' }, spec: {}, }); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); await expect(catalog.facets({ facets: ['kind'] })).resolves.toEqual({ facets: { @@ -804,9 +1479,9 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'can match on annotations and labels with dots in them, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); - await addEntityToSearch(knex, { + await addEntityToSearch({ apiVersion: 'a', kind: 'k', metadata: { @@ -816,7 +1491,7 @@ describe('DefaultEntitiesCatalog', () => { }, spec: {}, }); - await addEntityToSearch(knex, { + await addEntityToSearch({ apiVersion: 'a', kind: 'k', metadata: { @@ -826,7 +1501,11 @@ describe('DefaultEntitiesCatalog', () => { }, spec: {}, }); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); await expect( catalog.facets({ @@ -851,9 +1530,9 @@ describe('DefaultEntitiesCatalog', () => { it.each(databases.eachSupportedId())( 'can match on strings in arrays, %p', async databaseId => { - const { knex } = await createDatabase(databaseId); + await createDatabase(databaseId); - await addEntityToSearch(knex, { + await addEntityToSearch({ apiVersion: 'a', kind: 'k', metadata: { @@ -862,7 +1541,7 @@ describe('DefaultEntitiesCatalog', () => { }, spec: {}, }); - await addEntityToSearch(knex, { + await addEntityToSearch({ apiVersion: 'a', kind: 'k', metadata: { @@ -871,7 +1550,11 @@ describe('DefaultEntitiesCatalog', () => { }, spec: {}, }); - const catalog = new DefaultEntitiesCatalog(knex, stitcher); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); await expect( catalog.facets({ @@ -891,3 +1574,19 @@ describe('DefaultEntitiesCatalog', () => { ); }); }); + +function entityFrom( + name: string, + { uid, namespace }: { uid?: string; namespace?: string } = {}, +) { + return { + apiVersion: 'a', + kind: 'k', + metadata: { + name, + ...(!!namespace && { namespace }), + ...(!!uid && { uid }), + }, + spec: { should_include_this: 'yes' }, + }; +} diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index d598881371..4e6109423e 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -20,11 +20,14 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { InputError, NotFoundError } from '@backstage/errors'; -import lodash from 'lodash'; import { Knex } from 'knex'; +import { isEqual, chunk as lodashChunk } from 'lodash'; +import { Logger } from 'winston'; +import { z } from 'zod'; import { EntitiesBatchRequest, EntitiesBatchResponse, + Cursor, EntitiesCatalog, EntitiesRequest, EntitiesResponse, @@ -34,6 +37,9 @@ import { EntityFacetsResponse, EntityFilter, EntityPagination, + QueryEntitiesRequest, + QueryEntitiesResponse, + EntityOrder, } from '../catalog/types'; import { DbFinalEntitiesRow, @@ -43,8 +49,21 @@ import { DbRelationsRow, DbSearchRow, } from '../database/tables'; + import { Stitcher } from '../stitching/Stitcher'; +import { + isQueryEntitiesCursorRequest, + isQueryEntitiesInitialRequest, +} from './util'; + +const defaultSortField: EntityOrder = { + field: 'metadata.uid', + order: 'asc', +}; + +const DEFAULT_LIMIT = 20; + function parsePagination(input?: EntityPagination): { limit?: number; offset?: number; @@ -168,10 +187,15 @@ function parseFilter( } export class DefaultEntitiesCatalog implements EntitiesCatalog { - constructor( - private readonly database: Knex, - private readonly stitcher: Stitcher, - ) {} + private readonly database: Knex; + private readonly logger: Logger; + private readonly stitcher: Stitcher; + + constructor(options: { database: Knex; logger: Logger; stitcher: Stitcher }) { + this.database = options.database; + this.logger = options.logger; + this.stitcher = options.stitcher; + } async entities(request?: EntitiesRequest): Promise<EntitiesResponse> { const db = this.database; @@ -283,19 +307,29 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { ): Promise<EntitiesBatchResponse> { const lookup = new Map<string, Entity>(); - for (const chunk of lodash.chunk(request.entityRefs, 200)) { + for (const chunk of lodashChunk(request.entityRefs, 200)) { let query = this.database<DbFinalEntitiesRow>('final_entities') - .innerJoin<DbRefreshStateRow>('refresh_state', { - 'refresh_state.entity_id': 'final_entities.entity_id', - }) + .innerJoin<DbRefreshStateRow>( + 'refresh_state', + 'refresh_state.entity_id', + 'final_entities.entity_id', + ) .select({ entityRef: 'refresh_state.entity_ref', entity: 'final_entities.final_entity', }) .whereIn('refresh_state.entity_ref', chunk); + if (request?.filter) { - query = parseFilter(request.filter, query, this.database); + query = parseFilter( + request.filter, + query, + this.database, + false, + 'refresh_state.entity_id', + ); } + for (const row of await query) { lookup.set(row.entityRef, row.entity ? JSON.parse(row.entity) : null); } @@ -310,6 +344,161 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { return { items }; } + async queryEntities( + request: QueryEntitiesRequest, + ): Promise<QueryEntitiesResponse> { + const db = this.database; + + const limit = request.limit ?? DEFAULT_LIMIT; + + const cursor: Omit<Cursor, 'orderFieldValues'> & { + orderFieldValues?: (string | null)[]; + } = { + orderFields: [defaultSortField], + isPrevious: false, + ...parseCursorFromRequest(request), + }; + + const isFetchingBackwards = cursor.isPrevious; + + if (cursor.orderFields.length > 1) { + this.logger.warn(`Only one sort field is supported, ignoring the rest`); + } + + const sortField: EntityOrder = { + ...defaultSortField, + ...cursor.orderFields[0], + }; + + const [prevItemOrderFieldValue, prevItemUid] = + cursor.orderFieldValues || []; + + const dbQuery = db('search') + .join('final_entities', 'search.entity_id', 'final_entities.entity_id') + .where('search.key', sortField.field); + + if (cursor.filter) { + parseFilter(cursor.filter, dbQuery, db, false, 'search.entity_id'); + } + + const normalizedFullTextFilterTerm = cursor.fullTextFilter?.term?.trim(); + if (normalizedFullTextFilterTerm) { + dbQuery.andWhereRaw( + 'value like ?', + `%${normalizedFullTextFilterTerm.toLocaleLowerCase('en-US')}%`, + ); + } + + const countQuery = dbQuery.clone(); + + const isOrderingDescending = sortField.order === 'desc'; + + if (prevItemOrderFieldValue) { + dbQuery.andWhere( + 'value', + isFetchingBackwards !== isOrderingDescending ? '<' : '>', + prevItemOrderFieldValue, + ); + dbQuery.orWhere(function nested() { + this.where('value', '=', prevItemOrderFieldValue).andWhere( + 'search.entity_id', + isFetchingBackwards !== isOrderingDescending ? '<' : '>', + prevItemUid, + ); + }); + } + + dbQuery + .orderBy([ + { + column: 'value', + order: isFetchingBackwards + ? invertOrder(sortField.order) + : sortField.order, + }, + { + column: 'search.entity_id', + order: isFetchingBackwards + ? invertOrder(sortField.order) + : sortField.order, + }, + ]) + // fetch an extra item to check if there are more items. + .limit(isFetchingBackwards ? limit : limit + 1); + + countQuery.count('search.entity_id', { as: 'count' }); + + const [rows, [{ count }]] = await Promise.all([ + dbQuery, + // for performance reasons we invoke the countQuery + // only on the first request. + // The result is then embedded into the cursor + // for subsequent requests. + typeof cursor.totalItems === 'undefined' + ? countQuery + : [{ count: cursor.totalItems }], + ]); + + const totalItems = Number(count); + + if (isFetchingBackwards) { + rows.reverse(); + } + const hasMoreResults = + limit > 0 && (isFetchingBackwards || rows.length > limit); + + // discard the extra item only when fetching forward. + if (rows.length > limit) { + rows.length -= 1; + } + + const isInitialRequest = cursor.firstSortFieldValues === undefined; + + const firstRow = rows[0]; + const lastRow = rows[rows.length - 1]; + + const firstSortFieldValues = cursor.firstSortFieldValues || [ + firstRow?.value, + firstRow?.entity_id, + ]; + + const nextCursor: Cursor | undefined = hasMoreResults + ? { + ...cursor, + orderFieldValues: sortFieldsFromRow(lastRow), + firstSortFieldValues, + isPrevious: false, + totalItems, + } + : undefined; + + const prevCursor: Cursor | undefined = + !isInitialRequest && + rows.length > 0 && + !isEqual(sortFieldsFromRow(firstRow), cursor.firstSortFieldValues) + ? { + ...cursor, + orderFieldValues: sortFieldsFromRow(firstRow), + firstSortFieldValues: cursor.firstSortFieldValues, + isPrevious: true, + totalItems, + } + : undefined; + + const items = rows + .map(e => JSON.parse(e.final_entity!)) + .map(e => (request.fields ? request.fields(e) : e)); + + return { + items, + pageInfo: { + ...(!!prevCursor && { prevCursor }), + ...(!!nextCursor && { nextCursor }), + }, + totalItems, + }; + } + async removeEntityByUid(uid: string): Promise<void> { const dbConfig = this.database.client.config; @@ -490,3 +679,51 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { return { facets }; } } + +const entityFilterParser: z.ZodSchema<EntityFilter> = z.lazy(() => + z + .object({ + key: z.string(), + values: z.array(z.string()).optional(), + }) + .or(z.object({ not: entityFilterParser })) + .or(z.object({ anyOf: z.array(entityFilterParser) })) + .or(z.object({ allOf: z.array(entityFilterParser) })), +); + +export const cursorParser: z.ZodSchema<Cursor> = z.object({ + orderFields: z.array( + z.object({ field: z.string(), order: z.enum(['asc', 'desc']) }), + ), + orderFieldValues: z.array(z.string().or(z.null())), + filter: entityFilterParser.optional(), + isPrevious: z.boolean(), + query: z.string().optional(), + firstSortFieldValues: z.array(z.string().or(z.null())).optional(), + totalItems: z.number().optional(), +}); + +function parseCursorFromRequest( + request?: QueryEntitiesRequest, +): Partial<Cursor> { + if (isQueryEntitiesInitialRequest(request)) { + const { + filter, + orderFields: sortFields = [defaultSortField], + fullTextFilter, + } = request; + return { filter, orderFields: sortFields, fullTextFilter }; + } + if (isQueryEntitiesCursorRequest(request)) { + return request.cursor; + } + return {}; +} + +function invertOrder(order: EntityOrder['order']) { + return order === 'asc' ? 'desc' : 'asc'; +} + +function sortFieldsFromRow(row: DbSearchRow) { + return [row.value, row.entity_id]; +} diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 6379d65043..df934eae68 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -25,7 +25,7 @@ import { } from '@backstage/catalog-model'; import express from 'express'; import request from 'supertest'; -import { EntitiesCatalog } from '../catalog/types'; +import { Cursor, EntitiesCatalog } from '../catalog/types'; import { LocationInput, LocationService, RefreshService } from './types'; import { basicEntityFilter } from './request'; import { createRouter } from './createRouter'; @@ -34,9 +34,10 @@ import { createPermissionIntegrationRouter, createPermissionRule, } from '@backstage/plugin-permission-node'; -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { CatalogProcessingOrchestrator } from '../processing/types'; import { z } from 'zod'; +import { encodeCursor } from './util'; describe('createRouter readonly disabled', () => { let entitiesCatalog: jest.Mocked<EntitiesCatalog>; @@ -52,6 +53,7 @@ describe('createRouter readonly disabled', () => { removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), facets: jest.fn(), + queryEntities: jest.fn(), }; locationService = { getLocation: jest.fn(), @@ -135,6 +137,118 @@ describe('createRouter readonly disabled', () => { }); }); + describe('GET /entities/by-query', () => { + it('happy path: lists entities', async () => { + const items: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; + + entitiesCatalog.queryEntities.mockResolvedValueOnce({ + items, + totalItems: 100, + pageInfo: { nextCursor: mockCursor() }, + }); + + const response = await request(app).get('/entities/by-query'); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + items, + totalItems: 100, + pageInfo: { + nextCursor: expect.any(String), + }, + }); + }); + + it('parses initial request', async () => { + entitiesCatalog.queryEntities.mockResolvedValueOnce({ + items: [], + pageInfo: {}, + totalItems: 0, + }); + const response = await request(app).get( + '/entities/by-query?filter=a=1,a=2,b=3&filter=c=4&orderField=metadata.name,asc&orderField=metadata.uid,desc', + ); + + expect(response.status).toEqual(200); + expect(entitiesCatalog.queryEntities).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith({ + filter: { + anyOf: [ + { + allOf: [ + { key: 'a', values: ['1', '2'] }, + { key: 'b', values: ['3'] }, + ], + }, + { allOf: [{ key: 'c', values: ['4'] }] }, + ], + }, + orderFields: [ + { field: 'metadata.name', order: 'asc' }, + { field: 'metadata.uid', order: 'desc' }, + ], + fullTextFilter: { + fields: undefined, + term: '', + }, + }); + }); + + it('parses cursor request', async () => { + const items: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; + + entitiesCatalog.queryEntities.mockResolvedValueOnce({ + items, + totalItems: 100, + pageInfo: { nextCursor: mockCursor() }, + }); + + const cursor = mockCursor({ totalItems: 100, isPrevious: false }); + + const response = await request(app).get( + `/entities/by-query?cursor=${encodeCursor(cursor)}`, + ); + expect(entitiesCatalog.queryEntities).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith({ + cursor, + }); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + items, + totalItems: 100, + pageInfo: { nextCursor: expect.any(String) }, + }); + }); + + it('should throw in case of malformed cursor', async () => { + const items: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; + + entitiesCatalog.queryEntities.mockResolvedValueOnce({ + items, + totalItems: 100, + pageInfo: { nextCursor: mockCursor() }, + }); + + let response = await request(app).get( + `/entities/by-query?cursor=${Buffer.from( + JSON.stringify({ bad: 'cursor' }), + 'utf8', + ).toString('base64')}`, + ); + expect(response.status).toEqual(400); + expect(response.body.error.message).toMatch(/Malformed cursor/); + + response = await request(app).get(`/entities/by-query?cursor=badcursor`); + expect(response.status).toEqual(400); + expect(response.body.error.message).toMatch(/Malformed cursor/); + }); + }); + describe('GET /entities/by-uid/:uid', () => { it('can fetch entity by uid', async () => { const entity: Entity = { @@ -560,6 +674,7 @@ describe('createRouter readonly enabled', () => { removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), facets: jest.fn(), + queryEntities: jest.fn(), }; locationService = { getLocation: jest.fn(), @@ -750,6 +865,7 @@ describe('NextRouter permissioning', () => { removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), facets: jest.fn(), + queryEntities: jest.fn(), }; locationService = { getLocation: jest.fn(), @@ -820,3 +936,12 @@ describe('NextRouter permissioning', () => { }); }); }); + +function mockCursor(partialCursor?: Partial<Cursor>): Cursor { + return { + orderFields: [], + orderFieldValues: [], + isPrevious: false, + ...partialCursor, + }; +} diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 62a0f970c9..849096c601 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -39,12 +39,14 @@ import { parseEntityFilterParams, parseEntityPaginationParams, parseEntityTransformParams, + parseQueryEntitiesParams, } from './request'; import { parseEntityFacetParams } from './request/parseEntityFacetParams'; import { parseEntityOrderParams } from './request/parseEntityOrderParams'; import { LocationService, RefreshOptions, RefreshService } from './types'; import { disallowReadonlyMode, + encodeCursor, locationInput, validateRequestBody, } from './util'; @@ -130,6 +132,26 @@ export async function createRouter( // TODO(freben): encode the pageInfo in the response res.json(entities); }) + .get('/entities/by-query', async (req, res) => { + const { items, pageInfo, totalItems } = + await entitiesCatalog.queryEntities({ + ...parseQueryEntitiesParams(req.query), + authorizationToken: getBearerToken(req.header('authorization')), + }); + + res.json({ + items, + totalItems, + pageInfo: { + ...(pageInfo.nextCursor && { + nextCursor: encodeCursor(pageInfo.nextCursor), + }), + ...(pageInfo.prevCursor && { + prevCursor: encodeCursor(pageInfo.prevCursor), + }), + }, + }); + }) .get('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; const { entities } = await entitiesCatalog.entities({ diff --git a/plugins/catalog-backend/src/service/index.ts b/plugins/catalog-backend/src/service/index.ts index 7e0c6025c3..44645d39fe 100644 --- a/plugins/catalog-backend/src/service/index.ts +++ b/plugins/catalog-backend/src/service/index.ts @@ -14,6 +14,8 @@ * limitations under the License. */ -export type { CatalogEnvironment } from './CatalogBuilder'; +export type { + CatalogEnvironment, + CatalogPermissionRuleInput, +} from './CatalogBuilder'; export { CatalogBuilder } from './CatalogBuilder'; -export { catalogPlugin } from './CatalogPlugin'; diff --git a/plugins/catalog-backend/src/service/request/index.ts b/plugins/catalog-backend/src/service/request/index.ts index 0b09355e72..d9297fc122 100644 --- a/plugins/catalog-backend/src/service/request/index.ts +++ b/plugins/catalog-backend/src/service/request/index.ts @@ -19,3 +19,4 @@ export { basicEntityFilter } from './basicEntityFilter'; export { parseEntityFilterParams } from './parseEntityFilterParams'; export { parseEntityPaginationParams } from './parseEntityPaginationParams'; export { parseEntityTransformParams } from './parseEntityTransformParams'; +export { parseQueryEntitiesParams } from './parseQueryEntitiesParams'; diff --git a/plugins/catalog-backend/src/service/request/parseEntityOrderFieldParams.test.ts b/plugins/catalog-backend/src/service/request/parseEntityOrderFieldParams.test.ts new file mode 100644 index 0000000000..b732163394 --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseEntityOrderFieldParams.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; + +describe('parseEntityOrderFieldParams', () => { + it('supports no order fields', () => { + const result = parseEntityOrderFieldParams({}); + expect(result).toEqual(undefined); + }); + + it('supports single orderField', () => { + const result = parseEntityOrderFieldParams({ + orderField: ['metadata.name,desc'], + })!; + expect(result).toEqual([{ field: 'metadata.name', order: 'desc' }]); + }); + + it('supports single orderField without order', () => { + const result = parseEntityOrderFieldParams({ + orderField: ['metadata.name'], + })!; + expect(result).toEqual([{ field: 'metadata.name' }]); + }); + + it('supports multiple order fields', () => { + const result = parseEntityOrderFieldParams({ + orderField: ['metadata.name,desc', 'metadata.uid,asc'], + }); + + expect(result).toEqual([ + { field: 'metadata.name', order: 'desc' }, + { field: 'metadata.uid', order: 'asc' }, + ]); + }); + + it('throws if orderField order is not valid', () => { + expect(() => + parseEntityOrderFieldParams({ + orderField: ['metadata.name,desc', 'metadata.uid,invalid'], + }), + ).toThrow(/Invalid order field order/); + }); +}); diff --git a/plugins/catalog-backend/src/service/request/parseEntityOrderFieldParams.ts b/plugins/catalog-backend/src/service/request/parseEntityOrderFieldParams.ts new file mode 100644 index 0000000000..5ab8cbfcf9 --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseEntityOrderFieldParams.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { EntityOrder } from '../../catalog/types'; +import { parseStringsParam } from './common'; + +export function parseEntityOrderFieldParams( + params: Record<string, unknown>, +): EntityOrder[] | undefined { + const orderFieldStrings = parseStringsParam(params.orderField, 'orderField'); + if (!orderFieldStrings) { + return undefined; + } + + return orderFieldStrings.map(orderFieldString => { + const [field, order] = orderFieldString.split(','); + + if (order !== undefined && !isOrder(order)) { + throw new InputError('Invalid order field order, must be asc or desc'); + } + return { field, order }; + }); +} + +export function isOrder(order: string): order is 'asc' | 'desc' { + return ['asc', 'desc'].includes(order); +} diff --git a/plugins/catalog-backend/src/service/request/parseFullTextFilterFields.ts b/plugins/catalog-backend/src/service/request/parseFullTextFilterFields.ts new file mode 100644 index 0000000000..02ba5864fc --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseFullTextFilterFields.ts @@ -0,0 +1,28 @@ +import { parseStringParam } from './common'; + +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export function parseFullTextFilterFields(params: Record<string, unknown>) { + const fullTextFilterFields = parseStringParam( + params.fullTextFilterFields, + 'fullTextFilterFields', + ); + if (!fullTextFilterFields) { + return undefined; + } + + return fullTextFilterFields.split(','); +} diff --git a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.test.ts b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.test.ts new file mode 100644 index 0000000000..43ba218eac --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Cursor, + QueryEntitiesCursorRequest, + QueryEntitiesInitialRequest, +} from '../../catalog/types'; +import { encodeCursor } from '../util'; +import { parseQueryEntitiesParams } from './parseQueryEntitiesParams'; + +describe('parseQueryEntitiesParams', () => { + describe('initial request', () => { + it('should parse all the defined params', () => { + const validRequest = { + authorizationToken: 'to_not_be_returned', + fields: ['kind'], + limit: '3', + filter: ['a=1', 'b=2'], + orderField: ['metadata.name,desc'], + fullTextFilterTerm: 'query', + fullTextFilterFields: 'metadata.name,metadata.namespace', + }; + const parsedObj = parseQueryEntitiesParams( + validRequest, + ) as QueryEntitiesInitialRequest; + expect(parsedObj.limit).toBe(3); + expect(parsedObj.fields).toBeDefined(); + expect(parsedObj.orderFields).toEqual([ + { field: 'metadata.name', order: 'desc' }, + ]); + expect(parsedObj.filter).toBeDefined(); + expect(parsedObj.fullTextFilter).toEqual({ + term: 'query', + fields: ['metadata.name', 'metadata.namespace'], + }); + expect(parsedObj).not.toHaveProperty('authorizationToken'); + expect(parsedObj).not.toHaveProperty('cursor'); + }); + it('should ignore optional params', () => { + const parsedObj = parseQueryEntitiesParams( + {}, + ) as QueryEntitiesInitialRequest; + expect(parsedObj.limit).toBeUndefined(); + expect(parsedObj.fields).toBeUndefined(); + expect(parsedObj.orderFields).toBeUndefined(); + expect(parsedObj.filter).toBeUndefined(); + expect(parsedObj.fullTextFilter).toEqual({ term: '', fields: undefined }); + expect(parsedObj).not.toHaveProperty('authorizationToken'); + expect(parsedObj).not.toHaveProperty('cursor'); + }); + + it.each([ + { + limit: 'asd', + }, + { filter: 3 }, + { orderField: ['metadata.uid,diagonal'] }, + { fields: [4] }, + { fullTextFilterTerm: [] }, + { fullTextFilterFields: 3 }, + ])('should throw if some parameter is not valid %p', params => { + expect(() => parseQueryEntitiesParams(params)).toThrow(); + }); + }); + + describe('cursor request', () => { + it('should parse all the defined params', () => { + const cursor: Cursor = { + totalItems: 100, + orderFields: [], + orderFieldValues: [], + isPrevious: false, + }; + const validRequest = { + authorizationToken: 'to_not_be_returned', + fields: ['kind'], + limit: '3', + cursor: encodeCursor(cursor), + }; + const parsedObj = parseQueryEntitiesParams( + validRequest, + ) as QueryEntitiesCursorRequest; + expect(parsedObj.limit).toBe(3); + expect(parsedObj.fields).toBeDefined(); + expect(parsedObj.cursor).toEqual(cursor); + }); + + it('should ignore unknown params', () => { + const cursor: Cursor = { + totalItems: 100, + orderFields: [], + orderFieldValues: [], + isPrevious: false, + }; + const validRequest = { + authorizationToken: 'to_not_be_returned', + fields: ['kind'], + limit: '3', + cursor: encodeCursor(cursor), + filter: ['a=1', 'b=2'], + orderField: 'orderField,desc', + query: 'query', + }; + const parsedObj = parseQueryEntitiesParams( + validRequest, + ) as QueryEntitiesCursorRequest; + expect(parsedObj.limit).toBe(3); + expect(parsedObj.fields).toBeDefined(); + expect(parsedObj.cursor).toEqual(cursor); + expect(parsedObj).not.toHaveProperty('filter'); + expect(parsedObj).not.toHaveProperty('orderField'); + expect(parsedObj).not.toHaveProperty('query'); + }); + + it('should ignore optional params', () => { + const parsedObj = parseQueryEntitiesParams( + {}, + ) as QueryEntitiesCursorRequest; + expect(parsedObj.limit).toBeUndefined(); + expect(parsedObj.fields).toBeUndefined(); + }); + + it.each([ + { + limit: 'asd', + }, + { cursor: [] }, + { fields: [4] }, + ])('should throw if some parameter is not valid %p', params => { + expect(() => parseQueryEntitiesParams(params)).toThrow(); + }); + }); +}); diff --git a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts new file mode 100644 index 0000000000..ae8b649d13 --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + QueryEntitiesCursorRequest, + QueryEntitiesInitialRequest, + QueryEntitiesRequest, +} from '../../catalog/types'; +import { decodeCursor } from '../util'; +import { parseIntegerParam, parseStringParam } from './common'; +import { parseEntityFilterParams } from './parseEntityFilterParams'; +import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; +import { parseEntityTransformParams } from './parseEntityTransformParams'; +import { parseFullTextFilterFields } from './parseFullTextFilterFields'; + +export function parseQueryEntitiesParams( + params: Record<string, unknown>, +): Omit<QueryEntitiesRequest, 'authorizationToken'> { + const fields = parseEntityTransformParams(params); + const limit = parseIntegerParam(params.limit, 'limit'); + const cursor = parseStringParam(params.cursor, 'cursor'); + if (cursor) { + const decodedCursor = decodeCursor(cursor); + const response: Omit<QueryEntitiesCursorRequest, 'authorizationToken'> = { + cursor: decodedCursor, + fields, + limit, + }; + return response; + } + + const filter = parseEntityFilterParams(params); + const fullTextFilterTerm = parseStringParam( + params.fullTextFilterTerm, + 'fullTextFilterTerm', + ); + const fullTextFilterFields = parseFullTextFilterFields(params); + + const orderFields = parseEntityOrderFieldParams(params); + + const response: Omit<QueryEntitiesInitialRequest, 'authorizationToken'> = { + fields, + filter, + limit, + orderFields, + fullTextFilter: { + term: fullTextFilterTerm || '', + fields: fullTextFilterFields, + }, + }; + + return response; +} diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 9a914aadcb..699bca2fc9 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -18,6 +18,13 @@ import { InputError, NotAllowedError } from '@backstage/errors'; import { Request } from 'express'; import lodash from 'lodash'; import { z } from 'zod'; +import { + Cursor, + EntityFilter, + QueryEntitiesCursorRequest, + QueryEntitiesInitialRequest, + QueryEntitiesRequest, +} from '../catalog/types'; export async function requireRequestBody(req: Request): Promise<unknown> { const contentType = req.header('content-type'); @@ -65,3 +72,63 @@ export function disallowReadonlyMode(readonly: boolean) { throw new NotAllowedError('This operation not allowed in readonly mode'); } } + +export function isQueryEntitiesInitialRequest( + input: QueryEntitiesRequest | undefined, +): input is QueryEntitiesInitialRequest { + if (!input) { + return false; + } + return !isQueryEntitiesCursorRequest(input); +} + +export function isQueryEntitiesCursorRequest( + input: QueryEntitiesRequest | undefined, +): input is QueryEntitiesCursorRequest { + if (!input) { + return false; + } + return !!(input as QueryEntitiesCursorRequest).cursor; +} + +const entityFilterParser: z.ZodSchema<EntityFilter> = z.lazy(() => + z + .object({ + key: z.string(), + values: z.array(z.string()).optional(), + }) + .or(z.object({ not: entityFilterParser })) + .or(z.object({ anyOf: z.array(entityFilterParser) })) + .or(z.object({ allOf: z.array(entityFilterParser) })), +); + +export const cursorParser: z.ZodSchema<Cursor> = z.object({ + orderFields: z.array( + z.object({ field: z.string(), order: z.enum(['asc', 'desc']) }), + ), + orderFieldValues: z.array(z.string().or(z.null())), + filter: entityFilterParser.optional(), + isPrevious: z.boolean(), + query: z.string().optional(), + firstSortFieldValues: z.array(z.string().or(z.null())).optional(), + totalItems: z.number().optional(), +}); + +export function encodeCursor(cursor: Cursor) { + const json = JSON.stringify(cursor); + return Buffer.from(json, 'utf8').toString('base64'); +} + +export function decodeCursor(encodedCursor: string) { + try { + const data = Buffer.from(encodedCursor, 'base64').toString('utf8'); + const result = cursorParser.safeParse(JSON.parse(data)); + + if (!result.success) { + throw new InputError(`Malformed cursor: ${result.error}`); + } + return result.data; + } catch (e) { + throw new InputError(`Malformed cursor: ${e}`); + } +} diff --git a/plugins/catalog-backend/src/stitching/Stitcher.ts b/plugins/catalog-backend/src/stitching/Stitcher.ts index 4742c81933..38d0fd263f 100644 --- a/plugins/catalog-backend/src/stitching/Stitcher.ts +++ b/plugins/catalog-backend/src/stitching/Stitcher.ts @@ -15,10 +15,11 @@ */ import { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client'; +import { AlphaEntity, EntityStatusItem } from '@backstage/catalog-model/alpha'; import { - AlphaEntity, + ANNOTATION_EDIT_URL, + ANNOTATION_VIEW_URL, EntityRelation, - EntityStatusItem, } from '@backstage/catalog-model'; import { SerializedError, stringifyError } from '@backstage/errors'; import { Knex } from 'knex'; @@ -32,6 +33,11 @@ import { import { buildEntitySearch } from './buildEntitySearch'; import { BATCH_SIZE, generateStableHash } from './util'; +// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js +const scriptProtocolPattern = + // eslint-disable-next-line no-control-regex + /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; + /** * Performs the act of stitching - to take all of the various outputs from the * ingestion process, and stitching them together into the final entity JSON @@ -166,6 +172,14 @@ export class Stitcher { })); } } + // We opt to do this check here as we otherwise can't guarantee that it will be run after all processors + for (const annotation of [ANNOTATION_VIEW_URL, ANNOTATION_EDIT_URL]) { + const value = entity.metadata.annotations?.[annotation]; + if (typeof value === 'string' && scriptProtocolPattern.test(value)) { + entity.metadata.annotations![annotation] = + 'https://backstage.io/annotation-rejected-for-security-reasons'; + } + } // TODO: entityRef is lower case and should be uppercase in the final // result diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index f4f998d7db..753af21e7e 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-catalog-common +## 1.0.12-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.0.11 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.0.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.0.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + ## 1.0.10 ### Patch Changes diff --git a/plugins/catalog-common/alpha-api-report.md b/plugins/catalog-common/alpha-api-report.md new file mode 100644 index 0000000000..12e0806a9c --- /dev/null +++ b/plugins/catalog-common/alpha-api-report.md @@ -0,0 +1,45 @@ +## API Report File for "@backstage/plugin-catalog-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BasicPermission } from '@backstage/plugin-permission-common'; +import { ResourcePermission } from '@backstage/plugin-permission-common'; + +// @alpha +export const catalogEntityCreatePermission: BasicPermission; + +// @alpha +export const catalogEntityDeletePermission: ResourcePermission<'catalog-entity'>; + +// @alpha +export type CatalogEntityPermission = ResourcePermission< + typeof RESOURCE_TYPE_CATALOG_ENTITY +>; + +// @alpha +export const catalogEntityReadPermission: ResourcePermission<'catalog-entity'>; + +// @alpha +export const catalogEntityRefreshPermission: ResourcePermission<'catalog-entity'>; + +// @alpha +export const catalogLocationCreatePermission: BasicPermission; + +// @alpha +export const catalogLocationDeletePermission: BasicPermission; + +// @alpha +export const catalogLocationReadPermission: BasicPermission; + +// @alpha +export const catalogPermissions: ( + | BasicPermission + | ResourcePermission<'catalog-entity'> +)[]; + +// @alpha +export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-common/api-report.md b/plugins/catalog-common/api-report.md index 1bef626c79..231c8aac34 100644 --- a/plugins/catalog-common/api-report.md +++ b/plugins/catalog-common/api-report.md @@ -3,10 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BasicPermission } from '@backstage/plugin-permission-common'; import { Entity } from '@backstage/catalog-model'; import { IndexableDocument } from '@backstage/plugin-search-common'; -import { ResourcePermission } from '@backstage/plugin-permission-common'; // @public (undocumented) export type AnalyzeLocationEntityField = { @@ -44,12 +42,6 @@ export type AnalyzeLocationResponse = { generateEntities: AnalyzeLocationGenerateEntity[]; }; -// @alpha -export const catalogEntityCreatePermission: BasicPermission; - -// @alpha -export const catalogEntityDeletePermission: ResourcePermission<'catalog-entity'>; - // @public export interface CatalogEntityDocument extends IndexableDocument { // @deprecated (undocumented) @@ -66,39 +58,10 @@ export interface CatalogEntityDocument extends IndexableDocument { type: string; } -// @alpha -export type CatalogEntityPermission = ResourcePermission< - typeof RESOURCE_TYPE_CATALOG_ENTITY ->; - -// @alpha -export const catalogEntityReadPermission: ResourcePermission<'catalog-entity'>; - -// @alpha -export const catalogEntityRefreshPermission: ResourcePermission<'catalog-entity'>; - -// @alpha -export const catalogLocationCreatePermission: BasicPermission; - -// @alpha -export const catalogLocationDeletePermission: BasicPermission; - -// @alpha -export const catalogLocationReadPermission: BasicPermission; - -// @alpha -export const catalogPermissions: ( - | BasicPermission - | ResourcePermission<'catalog-entity'> -)[]; - // @public export type LocationSpec = { type: string; target: string; presence?: 'optional' | 'required'; }; - -// @alpha -export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; ``` diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 0f8aa3ea35..033ee10f3d 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,16 +1,27 @@ { "name": "@backstage/plugin-catalog-common", "description": "Common functionalities for the catalog plugin", - "version": "1.0.10", + "version": "1.0.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "common-library" @@ -25,7 +36,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -41,7 +52,6 @@ "@backstage/cli": "workspace:^" }, "files": [ - "dist", - "alpha" + "dist" ] } diff --git a/plugins/catalog-common/src/alpha.ts b/plugins/catalog-common/src/alpha.ts new file mode 100644 index 0000000000..2dfc028332 --- /dev/null +++ b/plugins/catalog-common/src/alpha.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + RESOURCE_TYPE_CATALOG_ENTITY, + catalogEntityReadPermission, + catalogEntityCreatePermission, + catalogEntityDeletePermission, + catalogEntityRefreshPermission, + catalogLocationReadPermission, + catalogLocationCreatePermission, + catalogLocationDeletePermission, + catalogPermissions, +} from './permissions'; +export type { CatalogEntityPermission } from './permissions'; diff --git a/plugins/catalog-common/src/index.ts b/plugins/catalog-common/src/index.ts index 95bd65bc92..52860a0e66 100644 --- a/plugins/catalog-common/src/index.ts +++ b/plugins/catalog-common/src/index.ts @@ -21,19 +21,6 @@ * @packageDocumentation */ -export { - RESOURCE_TYPE_CATALOG_ENTITY, - catalogEntityReadPermission, - catalogEntityCreatePermission, - catalogEntityDeletePermission, - catalogEntityRefreshPermission, - catalogLocationReadPermission, - catalogLocationCreatePermission, - catalogLocationDeletePermission, - catalogPermissions, -} from './permissions'; -export type { CatalogEntityPermission } from './permissions'; - export * from './search'; export * from './ingestion'; export type { LocationSpec } from './common'; diff --git a/plugins/catalog-customized/CHANGELOG.md b/plugins/catalog-customized/CHANGELOG.md index bba1464d15..dbcbd10520 100644 --- a/plugins/catalog-customized/CHANGELOG.md +++ b/plugins/catalog-customized/CHANGELOG.md @@ -1,5 +1,45 @@ # @internal/plugin-catalog-customized +## 0.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + +## 0.0.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.0.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.1 + +## 0.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + ## 0.0.6 ### Patch Changes diff --git a/plugins/catalog-customized/package.json b/plugins/catalog-customized/package.json index 5737cf6fea..3cacddf58a 100644 --- a/plugins/catalog-customized/package.json +++ b/plugins/catalog-customized/package.json @@ -1,7 +1,7 @@ { "name": "@internal/plugin-catalog-customized", "description": "The internal Backstage Customizable plugin for browsing the Backstage catalog", - "version": "0.0.6", + "version": "0.0.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 04448a0ccf..ec21c0aa66 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-catalog-graph +## 0.2.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.2.27 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- fe19058b74: Fix #16245: The CatalogGraphCard has parameter naming errors for CatalogGraphPage +- 1827710136: Expose additional props on the `CatalogGraphCard` to allow for custom node & label rendering or kind/relation filtering. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + +## 0.2.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + ## 0.2.26 ### Patch Changes diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index 33dee92935..f723523716 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -24,9 +24,10 @@ The plugin comes with these features: To use the catalog graph plugin, you have to add some things to your Backstage app: -1. Add a dependency to your `packages/app/package.json`, run: +1. Add a dependency to your `packages/app/package.json`: ```sh - yarn add @backstage/plugin-catalog-graph + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-catalog-graph ``` 2. Add the `CatalogGraphPage` to your `packages/app/src/App.tsx`: diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index 95f497fe95..f7444c5f1b 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -65,19 +65,13 @@ export enum Direction { } // @public -export const EntityCatalogGraphCard: (props: { - variant?: InfoCardVariants | undefined; - relationPairs?: RelationPairs | undefined; - maxDepth?: number | undefined; - unidirectional?: boolean | undefined; - mergeRelations?: boolean | undefined; - kinds?: string[] | undefined; - relations?: string[] | undefined; - direction?: Direction | undefined; - height?: number | undefined; - title?: string | undefined; - zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; -}) => JSX.Element; +export const EntityCatalogGraphCard: ( + props: Partial<EntityRelationsGraphProps> & { + variant?: InfoCardVariants | undefined; + height?: number | undefined; + title?: string | undefined; + }, +) => JSX.Element; // @public export type EntityEdge = DependencyGraphTypes.DependencyEdge<EntityEdgeData>; @@ -103,26 +97,27 @@ export type EntityNodeData = { }; // @public -export const EntityRelationsGraph: (props: { +export const EntityRelationsGraph: ( + props: EntityRelationsGraphProps, +) => JSX.Element; + +// @public (undocumented) +export type EntityRelationsGraphProps = { rootEntityNames: CompoundEntityRef | CompoundEntityRef[]; - maxDepth?: number | undefined; - unidirectional?: boolean | undefined; - mergeRelations?: boolean | undefined; - kinds?: string[] | undefined; - relations?: string[] | undefined; - direction?: Direction | undefined; - onNodeClick?: - | ((value: EntityNode, event: MouseEvent_2<unknown>) => void) - | undefined; - relationPairs?: RelationPairs | undefined; - className?: string | undefined; - zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; - renderNode?: DependencyGraphTypes.RenderNodeFunction<EntityNode> | undefined; - renderLabel?: - | DependencyGraphTypes.RenderLabelFunction<EntityEdge> - | undefined; - curve?: 'curveStepBefore' | 'curveMonotoneX' | undefined; -}) => JSX.Element; + maxDepth?: number; + unidirectional?: boolean; + mergeRelations?: boolean; + kinds?: string[]; + relations?: string[]; + direction?: Direction; + onNodeClick?: (value: EntityNode, event: MouseEvent_2<unknown>) => void; + relationPairs?: RelationPairs; + className?: string; + zoom?: 'enabled' | 'disabled' | 'enable-on-click'; + renderNode?: DependencyGraphTypes.RenderNodeFunction<EntityNode>; + renderLabel?: DependencyGraphTypes.RenderLabelFunction<EntityEdge>; + curve?: 'curveStepBefore' | 'curveMonotoneX'; +}; // @public export type RelationPairs = [string, string][]; diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 936ce63a5f..0e093a3ab1 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.26", + "version": "0.2.28-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index b9a15eb569..ae4ad850a4 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -36,8 +36,8 @@ import { Direction, EntityNode, EntityRelationsGraph, - RelationPairs, } from '../EntityRelationsGraph'; +import { EntityRelationsGraphProps } from '../EntityRelationsGraph/EntityRelationsGraph'; const useStyles = makeStyles<Theme, { height: number | undefined }>( { @@ -55,29 +55,26 @@ const useStyles = makeStyles<Theme, { height: number | undefined }>( { name: 'PluginCatalogGraphCatalogGraphCard' }, ); -export const CatalogGraphCard = (props: { - variant?: InfoCardVariants; - relationPairs?: RelationPairs; - maxDepth?: number; - unidirectional?: boolean; - mergeRelations?: boolean; - kinds?: string[]; - relations?: string[]; - direction?: Direction; - height?: number; - title?: string; - zoom?: 'enabled' | 'disabled' | 'enable-on-click'; -}) => { +export const CatalogGraphCard = ( + props: Partial<EntityRelationsGraphProps> & { + variant?: InfoCardVariants; + height?: number; + title?: string; + }, +) => { const { variant = 'gridItem', relationPairs = ALL_RELATION_PAIRS, maxDepth = 1, unidirectional = true, mergeRelations = true, + direction = Direction.LEFT_RIGHT, kinds, relations, - direction = Direction.LEFT_RIGHT, height, + className, + rootEntityNames, + onNodeClick, title = 'Relations', zoom = 'enable-on-click', } = props; @@ -90,7 +87,7 @@ export const CatalogGraphCard = (props: { const classes = useStyles({ height }); const analytics = useAnalytics(); - const onNodeClick = useCallback( + const defaultOnNodeClick = useCallback( (node: EntityNode, _: MouseEvent<unknown>) => { const nodeEntityName = parseEntityRef(node.id); const path = catalogEntityRoute({ @@ -113,8 +110,8 @@ export const CatalogGraphCard = (props: { rootEntityRefs: [stringifyEntityRef(entity)], unidirectional, mergeRelations, - kinds, - relations, + selectedKinds: kinds, + selectedRelations: relations, direction, }, { arrayFormat: 'brackets', addQueryPrefix: true }, @@ -133,15 +130,14 @@ export const CatalogGraphCard = (props: { }} > <EntityRelationsGraph - rootEntityNames={entityName} + {...props} + rootEntityNames={rootEntityNames || entityName} + onNodeClick={onNodeClick || defaultOnNodeClick} + className={className || classes.graph} maxDepth={maxDepth} unidirectional={unidirectional} mergeRelations={mergeRelations} - kinds={kinds} - relations={relations} direction={direction} - onNodeClick={onNodeClick} - className={classes.graph} relationPairs={relationPairs} zoom={zoom} /> diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 67aa230ce2..2451f4913a 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -67,11 +67,9 @@ const useStyles = makeStyles( ); /** - * Core building block for custom entity relations diagrams. - * * @public */ -export const EntityRelationsGraph = (props: { +export type EntityRelationsGraphProps = { rootEntityNames: CompoundEntityRef | CompoundEntityRef[]; maxDepth?: number; unidirectional?: boolean; @@ -86,7 +84,14 @@ export const EntityRelationsGraph = (props: { renderNode?: DependencyGraphTypes.RenderNodeFunction<EntityNode>; renderLabel?: DependencyGraphTypes.RenderLabelFunction<EntityEdge>; curve?: 'curveStepBefore' | 'curveMonotoneX'; -}) => { +}; + +/** + * Core building block for custom entity relations diagrams. + * + * @public + */ +export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { const { rootEntityNames, maxDepth = 2, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts index b86c8f05ae..06227babe3 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ export { EntityRelationsGraph } from './EntityRelationsGraph'; +export type { EntityRelationsGraphProps } from './EntityRelationsGraph'; export { ALL_RELATION_PAIRS } from './relations'; export type { RelationPairs } from './relations'; export { Direction } from './types'; diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 4b083110ca..ede51b17fb 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-catalog-graphql +## 0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.3.18 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + ## 0.3.17 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index e76c1fbb69..8fd4221a56 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.17", + "version": "0.3.19-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -48,12 +48,12 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", - "@graphql-codegen/cli": "^2.3.1", - "@graphql-codegen/graphql-modules-preset": "^2.3.2", - "@graphql-codegen/typescript": "^2.4.2", - "@graphql-codegen/typescript-resolvers": "^2.4.3", + "@graphql-codegen/cli": "^3.0.0", + "@graphql-codegen/graphql-modules-preset": "^3.0.0", + "@graphql-codegen/typescript": "^3.0.0", + "@graphql-codegen/typescript-resolvers": "^3.0.0", "@graphql-tools/schema": "^9.0.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index dcabceeea1..cf6e3068cc 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,80 @@ # @backstage/plugin-catalog-import +## 0.9.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + +## 0.9.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-catalog-common@1.0.11 + +## 0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.9.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + +## 0.9.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/integration-react@1.1.9 + ## 0.9.4 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index e7a07077d6..57633f7b22 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.9.4", + "version": "0.9.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -68,7 +68,7 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index 03e1392d47..6f4098a158 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-catalog-node +## 1.3.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 1.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + +## 1.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + +## 1.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + +## 1.3.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 1.3.1 ### Patch Changes diff --git a/plugins/catalog-node/alpha-api-report.md b/plugins/catalog-node/alpha-api-report.md new file mode 100644 index 0000000000..ba1bcd47b2 --- /dev/null +++ b/plugins/catalog-node/alpha-api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/plugin-catalog-node" + +> 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 { CatalogProcessor } from '@backstage/plugin-catalog-node'; +import { EntityProvider } from '@backstage/plugin-catalog-node'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; + +// @alpha (undocumented) +export interface CatalogProcessingExtensionPoint { + // (undocumented) + addEntityProvider( + ...providers: Array<EntityProvider | Array<EntityProvider>> + ): void; + // (undocumented) + addProcessor( + ...processors: Array<CatalogProcessor | Array<CatalogProcessor>> + ): void; +} + +// @alpha (undocumented) +export const catalogProcessingExtensionPoint: ExtensionPoint<CatalogProcessingExtensionPoint>; + +// @alpha +export const catalogServiceRef: ServiceRef<CatalogApi, 'plugin'>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-node/api-report.md b/plugins/catalog-node/api-report.md index 9e9045db84..87da0b6944 100644 --- a/plugins/catalog-node/api-report.md +++ b/plugins/catalog-node/api-report.md @@ -5,28 +5,10 @@ ```ts /// <reference types="node" /> -import { CatalogApi } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { JsonValue } from '@backstage/types'; import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common'; -import { ServiceRef } from '@backstage/backend-plugin-api'; - -// @alpha (undocumented) -export interface CatalogProcessingExtensionPoint { - // (undocumented) - addEntityProvider( - ...providers: Array<EntityProvider | Array<EntityProvider>> - ): void; - // (undocumented) - addProcessor( - ...processors: Array<CatalogProcessor | Array<CatalogProcessor>> - ): void; -} - -// @alpha (undocumented) -export const catalogProcessingExtensionPoint: ExtensionPoint<CatalogProcessingExtensionPoint>; // @public (undocumented) export type CatalogProcessor = { @@ -109,9 +91,6 @@ export type CatalogProcessorResult = | CatalogProcessorErrorResult | CatalogProcessorRefreshKeysResult; -// @alpha -export const catalogServiceRef: ServiceRef<CatalogApi, 'plugin'>; - // @public export type DeferredEntity = { entity: Entity; diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 7a5b8d71cf..4f122ec465 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,22 +1,34 @@ { "name": "@backstage/plugin-catalog-node", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", - "version": "1.3.1", + "version": "1.3.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "node-library" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -37,7 +49,6 @@ "@backstage/cli": "workspace:^" }, "files": [ - "dist", - "alpha" + "dist" ] } diff --git a/plugins/catalog-node/src/alpha.ts b/plugins/catalog-node/src/alpha.ts new file mode 100644 index 0000000000..4fb07bbc25 --- /dev/null +++ b/plugins/catalog-node/src/alpha.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { catalogServiceRef } from './catalogService'; +export type { CatalogProcessingExtensionPoint } from './extensions'; +export { catalogProcessingExtensionPoint } from './extensions'; diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index 8dd9d8579a..0466af36b0 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -14,8 +14,10 @@ * limitations under the License. */ import { createExtensionPoint } from '@backstage/backend-plugin-api'; -import { EntityProvider } from './api'; -import { CatalogProcessor } from './api/processor'; +import { + EntityProvider, + CatalogProcessor, +} from '@backstage/plugin-catalog-node'; /** * @alpha diff --git a/plugins/catalog-node/src/index.ts b/plugins/catalog-node/src/index.ts index 6393c55f31..6a1accfcbd 100644 --- a/plugins/catalog-node/src/index.ts +++ b/plugins/catalog-node/src/index.ts @@ -20,8 +20,5 @@ * @packageDocumentation */ -export type { CatalogProcessingExtensionPoint } from './extensions'; -export { catalogProcessingExtensionPoint } from './extensions'; -export { catalogServiceRef } from './catalogService'; export * from './api'; export * from './processing'; diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index edfc2cad6b..db92d850ed 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,104 @@ # @backstage/plugin-catalog-react +## 1.4.0-next.0 + +### Minor Changes + +- 0a5b73b292: Add possibility to re-use EntityPicker for filters with multiple select. + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.11-next.0 + +## 1.3.0 + +### Minor Changes + +- 929e1afe1b: Add `initialFilter` prop to EntityLifecyclePicker. This allows you to set an initial lifecycle for the catalog. +- fab93c2fe8: Aligned buttons on "Unregister entity" dialog to keep them on the same line + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.10 + +## 1.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + +## 1.3.0-next.1 + +### Minor Changes + +- fab93c2fe8: Aligned buttons on "Unregister entity" dialog to keep them on the same line + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + +## 1.3.0-next.0 + +### Minor Changes + +- 929e1afe1b: Add `initialFilter` prop to EntityLifecyclePicker. This allows you to set an initial lifecycle for the catalog. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + ## 1.2.4 ### Patch Changes diff --git a/plugins/catalog-react/alpha-api-report.md b/plugins/catalog-react/alpha-api-report.md new file mode 100644 index 0000000000..f9b740c07c --- /dev/null +++ b/plugins/catalog-react/alpha-api-report.md @@ -0,0 +1,22 @@ +## API Report File for "@backstage/plugin-catalog-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Entity } from '@backstage/catalog-model'; +import { ResourcePermission } from '@backstage/plugin-permission-common'; + +// @alpha +export function isOwnerOf(owner: Entity, entity: Entity): boolean; + +// @alpha +export function useEntityPermission( + permission: ResourcePermission<'catalog-entity'>, +): { + loading: boolean; + allowed: boolean; + error?: Error; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 9528c55326..55c72122f5 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -20,7 +20,6 @@ import { Overrides } from '@material-ui/core/styles/overrides'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; -import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { StyleRules } from '@material-ui/core/styles/withStyles'; @@ -194,7 +193,9 @@ export class EntityLifecycleFilter implements EntityFilter { } // @public (undocumented) -export const EntityLifecyclePicker: () => JSX.Element | null; +export const EntityLifecyclePicker: (props: { + initialFilter?: string[]; +}) => JSX.Element | null; // @public export const EntityListContext: React_2.Context< @@ -391,9 +392,7 @@ export class EntityTagFilter implements EntityFilter { } // @public (undocumented) -export const EntityTagPicker: ( - props: EntityTagPickerProps, -) => JSX.Element | null; +export const EntityTagPicker: (props: EntityTagPickerProps) => JSX.Element; // @public (undocumented) export type EntityTagPickerProps = { @@ -474,16 +473,15 @@ export function InspectEntityDialog(props: { onClose: () => void; }): JSX.Element | null; -// @alpha -export function isOwnerOf(owner: Entity, entity: Entity): boolean; - // @public (undocumented) -export const MockEntityListContextProvider: ({ +export function MockEntityListContextProvider< + T extends DefaultEntityFilters = DefaultEntityFilters, +>({ children, value, -}: React_2.PropsWithChildren<{ - value?: Partial<EntityListContextProps<DefaultEntityFilters>> | undefined; -}>) => JSX.Element; +}: PropsWithChildren<{ + value?: Partial<EntityListContextProps<T>>; +}>): JSX.Element; // @public export class MockStarredEntitiesApi implements StarredEntitiesApi { @@ -536,15 +534,6 @@ export function useEntityOwnership(): { isOwnedEntity: (entity: Entity) => boolean; }; -// @alpha -export function useEntityPermission( - permission: ResourcePermission<'catalog-entity'>, -): { - loading: boolean; - allowed: boolean; - error?: Error; -}; - // @public export function useEntityTypeFilter(): { loading: boolean; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 1f2babba77..7a707240ca 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "1.2.4", + "version": "1.4.0-next.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", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "web-library" @@ -24,7 +36,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -78,7 +90,6 @@ "react-test-renderer": "^16.13.1" }, "files": [ - "dist", - "alpha" + "dist" ] } diff --git a/plugins/catalog-react/src/alpha.ts b/plugins/catalog-react/src/alpha.ts new file mode 100644 index 0000000000..e8ff21609e --- /dev/null +++ b/plugins/catalog-react/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { isOwnerOf } from './utils'; +export { useEntityPermission } from './hooks/useEntityPermission'; diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.test.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.test.tsx new file mode 100644 index 0000000000..38edb9b064 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.test.tsx @@ -0,0 +1,272 @@ +/* + * 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 { fireEvent, render, waitFor, screen } from '@testing-library/react'; +import React from 'react'; +import { MockEntityListContextProvider } from '../../testUtils/providers'; +import { EntityAutocompletePicker } from './EntityAutocompletePicker'; +import { TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '../../api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { DefaultEntityFilters } from '../../hooks'; +import { Entity } from '@backstage/catalog-model'; +import { EntityFilter } from '../../types'; + +interface EntityFilters extends DefaultEntityFilters { + options?: EntityOptionFilter; +} + +const options = ['option1', 'option2', 'option3', 'option4']; + +class EntityOptionFilter implements EntityFilter { + constructor(readonly values: string[]) {} + + filterEntity(entity: Entity): boolean { + return this.values.every(v => + ((entity.spec?.options ?? []) as string[]).includes(v), + ); + } + + toQueryValue(): string[] { + return this.values; + } +} + +describe('<EntityAutocompletePicker/>', () => { + const mockCatalogApi = { + getEntityFacets: async () => ({ + facets: { + 'spec.options': options.map((value, idx) => ({ value, count: idx })), + }, + }), + } as unknown as CatalogApi; + + it('renders all options', async () => { + render( + <TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}> + <MockEntityListContextProvider value={{}}> + <EntityAutocompletePicker<EntityFilters> + label="Options" + path="spec.options" + name="options" + Filter={EntityOptionFilter} + /> + </MockEntityListContextProvider> + </TestApiProvider>, + ); + await waitFor(() => + expect(screen.getByText('Options')).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByTestId('options-picker-expand')); + options.forEach(option => { + expect(screen.getByText(option)).toBeInTheDocument(); + }); + }); + + it('renders unique options in alphabetical order', async () => { + render( + <TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}> + <MockEntityListContextProvider value={{}}> + <EntityAutocompletePicker<EntityFilters> + label="Options" + path="spec.options" + name="options" + Filter={EntityOptionFilter} + /> + </MockEntityListContextProvider> + </TestApiProvider>, + ); + await waitFor(() => + expect(screen.getByText('Options')).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByTestId('options-picker-expand')); + + expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([ + 'option1', + 'option2', + 'option3', + 'option4', + ]); + }); + + it('renders options with counts', async () => { + render( + <TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}> + <MockEntityListContextProvider value={{}}> + <EntityAutocompletePicker<EntityFilters> + label="Options" + path="spec.options" + name="options" + Filter={EntityOptionFilter} + showCounts + /> + </MockEntityListContextProvider> + </TestApiProvider>, + ); + await waitFor(() => + expect(screen.getByText('Options')).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByTestId('options-picker-expand')); + + expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([ + 'option1 (0)', + 'option2 (1)', + 'option3 (2)', + 'option4 (3)', + ]); + }); + + it('respects the query parameter filter value', async () => { + const updateFilters = jest.fn(); + const queryParameters = { options: ['option3'] }; + render( + <TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}> + <MockEntityListContextProvider<EntityFilters> + value={{ + updateFilters, + queryParameters, + }} + > + <EntityAutocompletePicker<EntityFilters> + label="Options" + path="spec.options" + name="options" + Filter={EntityOptionFilter} + /> + </MockEntityListContextProvider> + </TestApiProvider>, + ); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + options: new EntityOptionFilter(['option3']), + }), + ); + }); + + it('adds options to filters', async () => { + const updateFilters = jest.fn(); + render( + <TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}> + <MockEntityListContextProvider + value={{ + updateFilters, + }} + > + <EntityAutocompletePicker<EntityFilters> + label="Options" + path="spec.options" + name="options" + Filter={EntityOptionFilter} + /> + </MockEntityListContextProvider> + </TestApiProvider>, + ); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + options: undefined, + }), + ); + + fireEvent.click(screen.getByTestId('options-picker-expand')); + fireEvent.click(screen.getByText('option1')); + expect(updateFilters).toHaveBeenLastCalledWith({ + options: new EntityOptionFilter(['option1']), + }); + }); + + it('removes options from filters', async () => { + const updateFilters = jest.fn(); + render( + <TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}> + <MockEntityListContextProvider<EntityFilters> + value={{ + updateFilters, + filters: { options: new EntityOptionFilter(['option1']) }, + }} + > + <EntityAutocompletePicker<EntityFilters> + label="Options" + path="spec.options" + name="options" + Filter={EntityOptionFilter} + /> + </MockEntityListContextProvider> + </TestApiProvider>, + ); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + options: new EntityOptionFilter(['option1']), + }), + ); + fireEvent.click(screen.getByTestId('options-picker-expand')); + expect(screen.getByLabelText('option1')).toBeChecked(); + + fireEvent.click(screen.getByLabelText('option1')); + expect(updateFilters).toHaveBeenLastCalledWith({ + options: undefined, + }); + }); + + it('responds to external queryParameters changes', async () => { + const updateFilters = jest.fn(); + const rendered = render( + <TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}> + <MockEntityListContextProvider<EntityFilters> + value={{ + updateFilters, + queryParameters: { options: ['option1'] }, + }} + > + <EntityAutocompletePicker<EntityFilters> + label="Options" + path="spec.options" + name="options" + Filter={EntityOptionFilter} + /> + </MockEntityListContextProvider> + </TestApiProvider>, + ); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + options: new EntityOptionFilter(['option1']), + }), + ); + rendered.rerender( + <TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}> + <MockEntityListContextProvider<EntityFilters> + value={{ + updateFilters, + queryParameters: { options: ['option2'] }, + }} + > + <EntityAutocompletePicker<EntityFilters> + label="Options" + path="spec.options" + name="options" + Filter={EntityOptionFilter} + /> + </MockEntityListContextProvider> + </TestApiProvider>, + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + options: new EntityOptionFilter(['option2']), + }); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx new file mode 100644 index 0000000000..b81d681e6d --- /dev/null +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -0,0 +1,155 @@ +/* + * 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 { Box, TextFieldProps, Typography } from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { Autocomplete } from '@material-ui/lab'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { catalogApiRef } from '../../api'; +import { EntityAutocompletePickerOption } from './EntityAutocompletePickerOption'; +import { EntityAutocompletePickerInput } from './EntityAutocompletePickerInput'; +import { + DefaultEntityFilters, + useEntityList, +} from '../../hooks/useEntityListProvider'; +import { EntityFilter } from '../../types'; +import _ from 'lodash'; + +type KeysMatchingCondition<T, V, K> = T extends V ? K : never; +type KeysMatching<T, V> = { + [K in keyof T]-?: KeysMatchingCondition<T[K], V, K>; +}[keyof T]; + +type AllowedEntityFilters<T extends DefaultEntityFilters> = KeysMatching< + T, + EntityFilter & { values: string[] } +>; + +interface ConstructableFilter<T> { + new (values: string[]): T; +} + +/** @public */ +export type EntityAutocompletePickerProps< + T extends DefaultEntityFilters = DefaultEntityFilters, + Name extends AllowedEntityFilters<T> = AllowedEntityFilters<T>, +> = { + label: string; + name: Name; + path: string; + showCounts?: boolean; + Filter: ConstructableFilter<NonNullable<T[Name]>>; + InputProps?: TextFieldProps; +}; + +/** @public */ +export function EntityAutocompletePicker< + T extends DefaultEntityFilters = DefaultEntityFilters, + Name extends AllowedEntityFilters<T> = AllowedEntityFilters<T>, +>(props: EntityAutocompletePickerProps<T, Name>) { + const { label, name, path, showCounts, Filter, InputProps } = props; + + const { + updateFilters, + filters, + queryParameters: { [name]: queryParameter }, + } = useEntityList<T>(); + + const catalogApi = useApi(catalogApiRef); + const { value: availableValues } = useAsync(async () => { + const facet = path; + const { facets } = await catalogApi.getEntityFacets({ + facets: [facet], + filter: filters.kind?.getCatalogFilters(), + }); + + return Object.fromEntries( + facets[facet].map(({ value, count }) => [value, count]), + ); + }, [filters.kind]); + + const queryParameters = useMemo( + () => [queryParameter].flat().filter(Boolean) as string[], + [queryParameter], + ); + + const [selectedOptions, setSelectedOptions] = useState( + queryParameters.length + ? queryParameters + : (filters[name] as unknown as { values: string[] })?.values ?? [], + ); + + // Set selected options on query parameter updates; this happens at initial page load and from + // external updates to the page location + useEffect(() => { + if ( + queryParameters.length && + !_.isEqual(selectedOptions, queryParameters) + ) { + setSelectedOptions(queryParameters); + } + }, [selectedOptions, queryParameters]); + + const availableOptions = Object.keys(availableValues ?? {}); + const shouldAddFilter = selectedOptions.length && availableOptions.length; + + useEffect(() => { + updateFilters({ + [name]: shouldAddFilter ? new Filter(selectedOptions) : undefined, + } as Partial<T>); + }, [name, shouldAddFilter, selectedOptions, Filter, updateFilters]); + + if ( + (filters[name] && !('values' in filters[name])) || + !availableOptions.length + ) { + return null; + } + + return ( + <Box pb={1} pt={1}> + <Typography variant="button" component="label"> + {label} + <Autocomplete + multiple + disableCloseOnSelect + options={availableOptions} + value={selectedOptions} + onChange={(_event: object, options: string[]) => + setSelectedOptions(options) + } + renderOption={(option, { selected }) => ( + <EntityAutocompletePickerOption + selected={selected} + value={option} + availableOptions={availableValues} + showCounts={!!showCounts} + /> + )} + size="small" + popupIcon={ + <ExpandMoreIcon data-testid={`${String(name)}-picker-expand`} /> + } + renderInput={params => ( + <EntityAutocompletePickerInput {...params} {...InputProps} /> + )} + /> + </Typography> + </Box> + ); +} diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerInput.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerInput.tsx new file mode 100644 index 0000000000..4140feae01 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerInput.tsx @@ -0,0 +1,39 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { makeStyles, TextField, TextFieldProps } from '@material-ui/core'; +import React from 'react'; +import classnames from 'classnames'; + +const useStyles = makeStyles( + { + input: {}, + }, + { + name: 'CatalogReactEntityAutocompletePickerInput', + }, +); + +export function EntityAutocompletePickerInput(params: TextFieldProps) { + const classes = useStyles(); + + return ( + <TextField + variant="outlined" + {...params} + className={classnames(classes.input, params.className)} + /> + ); +} diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerOption.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerOption.tsx new file mode 100644 index 0000000000..51b57257a9 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerOption.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Checkbox, FormControlLabel } from '@material-ui/core'; +import CheckBoxIcon from '@material-ui/icons/CheckBox'; +import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; +import React, { memo } from 'react'; + +interface Props { + selected: boolean; + value: string; + availableOptions?: Record<string, number>; + showCounts: boolean; +} + +const icon = <CheckBoxOutlineBlankIcon fontSize="small" />; +const checkedIcon = <CheckBoxIcon fontSize="small" />; + +function OptionCheckbox({ selected }: { selected: boolean }) { + return <Checkbox icon={icon} checkedIcon={checkedIcon} checked={selected} />; +} + +export const EntityAutocompletePickerOption = memo((props: Props) => { + const { selected, value, availableOptions, showCounts } = props; + const label = showCounts ? `${value} (${availableOptions?.[value]})` : value; + + return ( + <FormControlLabel + control={<OptionCheckbox selected={selected} />} + label={label} + /> + ); +}); diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts b/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts new file mode 100644 index 0000000000..10ce0123b7 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { EntityAutocompletePicker } from './EntityAutocompletePicker'; +export type { EntityAutocompletePickerProps } from './EntityAutocompletePicker'; diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx index ef77f7615f..3ee610ddaf 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx @@ -210,4 +210,21 @@ describe('<EntityLifecyclePicker/>', () => { lifecycles: undefined, }); }); + it('responds to initialFilter prop', () => { + const updateFilters = jest.fn(); + render( + <MockEntityListContextProvider + value={{ + entities: sampleEntities, + backendEntities: sampleEntities, + updateFilters, + }} + > + <EntityLifecyclePicker initialFilter={['production']} /> + </MockEntityListContextProvider>, + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + lifecycles: new EntityLifecycleFilter(['production']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index b941f94531..29002564ca 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -47,7 +47,8 @@ const icon = <CheckBoxOutlineBlankIcon fontSize="small" />; const checkedIcon = <CheckBoxIcon fontSize="small" />; /** @public */ -export const EntityLifecyclePicker = () => { +export const EntityLifecyclePicker = (props: { initialFilter?: string[] }) => { + const { initialFilter = [] } = props; const classes = useStyles(); const { updateFilters, @@ -64,7 +65,7 @@ export const EntityLifecyclePicker = () => { const [selectedLifecycles, setSelectedLifecycles] = useState( queryParamLifecycles.length ? queryParamLifecycles - : filters.lifecycles?.values ?? [], + : filters.lifecycles?.values ?? initialFilter, ); // Set selected lifecycles on query parameter updates; this happens at initial page load and from @@ -104,6 +105,7 @@ export const EntityLifecyclePicker = () => { Lifecycle <Autocomplete multiple + disableCloseOnSelect options={availableLifecycles} value={selectedLifecycles} onChange={(_: object, value: string[]) => diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index b067bda0f5..c8a6c64dd6 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -108,6 +108,7 @@ export const EntityOwnerPicker = () => { Owner <Autocomplete multiple + disableCloseOnSelect options={availableOwners} value={selectedOwners} onChange={(_: object, value: string[]) => setSelectedOwners(value)} diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx index 3fe3b60e25..db48534bc0 100644 --- a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx @@ -74,6 +74,7 @@ export const EntityProcessingStatusPicker = () => { Processing Status <Autocomplete multiple + disableCloseOnSelect options={availableAdvancedItems} value={selectedAdvancedItems} onChange={(_: object, value: string[]) => { diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index de7bbb89be..c70217f3a4 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -44,7 +44,7 @@ describe('<EntityTagPicker/>', () => { ); await waitFor(() => expect(screen.getByText('Tags')).toBeInTheDocument()); - fireEvent.click(screen.getByTestId('tag-picker-expand')); + fireEvent.click(screen.getByTestId('tags-picker-expand')); tags.forEach(tag => { expect(screen.getByText(tag)).toBeInTheDocument(); }); @@ -60,7 +60,7 @@ describe('<EntityTagPicker/>', () => { ); await waitFor(() => expect(screen.getByText('Tags')).toBeInTheDocument()); - fireEvent.click(screen.getByTestId('tag-picker-expand')); + fireEvent.click(screen.getByTestId('tags-picker-expand')); expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([ 'tag1', @@ -80,7 +80,7 @@ describe('<EntityTagPicker/>', () => { ); await waitFor(() => expect(screen.getByText('Tags')).toBeInTheDocument()); - fireEvent.click(screen.getByTestId('tag-picker-expand')); + fireEvent.click(screen.getByTestId('tags-picker-expand')); expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([ 'tag1 (0)', @@ -132,7 +132,7 @@ describe('<EntityTagPicker/>', () => { }), ); - fireEvent.click(screen.getByTestId('tag-picker-expand')); + fireEvent.click(screen.getByTestId('tags-picker-expand')); fireEvent.click(screen.getByText('tag1')); expect(updateFilters).toHaveBeenLastCalledWith({ tags: new EntityTagFilter(['tag1']), @@ -158,7 +158,7 @@ describe('<EntityTagPicker/>', () => { tags: new EntityTagFilter(['tag1']), }), ); - fireEvent.click(screen.getByTestId('tag-picker-expand')); + fireEvent.click(screen.getByTestId('tags-picker-expand')); expect(screen.getByLabelText('tag1')).toBeChecked(); fireEvent.click(screen.getByLabelText('tag1')); diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index 4a1dc03e96..0521bb49f8 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -14,132 +14,36 @@ * limitations under the License. */ -import { - Box, - Checkbox, - FormControlLabel, - makeStyles, - TextField, - Typography, -} from '@material-ui/core'; -import CheckBoxIcon from '@material-ui/icons/CheckBox'; -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 { useEntityList } from '../../hooks/useEntityListProvider'; +import { makeStyles } from '@material-ui/core'; +import React from 'react'; import { EntityTagFilter } from '../../filters'; -import { useApi } from '@backstage/core-plugin-api'; -import useAsync from 'react-use/lib/useAsync'; -import { catalogApiRef } from '../../api'; +import { EntityAutocompletePicker } from '../EntityAutocompletePicker/EntityAutocompletePicker'; /** @public */ export type CatalogReactEntityTagPickerClassKey = 'input'; -const useStyles = makeStyles( - { - input: {}, - }, - { - name: 'CatalogReactEntityTagPicker', - }, -); - -const icon = <CheckBoxOutlineBlankIcon fontSize="small" />; -const checkedIcon = <CheckBoxIcon fontSize="small" />; - /** @public */ export type EntityTagPickerProps = { showCounts?: boolean; }; +const useStyles = makeStyles( + { input: {} }, + { name: 'CatalogReactEntityTagPicker' }, +); + /** @public */ export const EntityTagPicker = (props: EntityTagPickerProps) => { const classes = useStyles(); - const { - updateFilters, - filters, - queryParameters: { tags: tagsParameter }, - } = useEntityList(); - - const catalogApi = useApi(catalogApiRef); - const { value: availableTags } = useAsync(async () => { - const facet = 'metadata.tags'; - const { facets } = await catalogApi.getEntityFacets({ - facets: [facet], - filter: filters.kind?.getCatalogFilters(), - }); - - return Object.fromEntries( - facets[facet].map(({ value, count }) => [value, count]), - ); - }, [filters.kind]); - - const queryParamTags = useMemo( - () => [tagsParameter].flat().filter(Boolean) as string[], - [tagsParameter], - ); - - const [selectedTags, setSelectedTags] = useState( - queryParamTags.length ? queryParamTags : filters.tags?.values ?? [], - ); - - // Set selected tags on query parameter updates; this happens at initial page load and from - // external updates to the page location. - useEffect(() => { - if (queryParamTags.length) { - setSelectedTags(queryParamTags); - } - }, [queryParamTags]); - - useEffect(() => { - const tags = Object.keys(availableTags ?? {}); - updateFilters({ - tags: - selectedTags.length && tags.length - ? new EntityTagFilter(selectedTags) - : undefined, - }); - }, [selectedTags, updateFilters, availableTags]); - - if (!Object.keys(availableTags ?? {}).length) return null; return ( - <Box pb={1} pt={1}> - <Typography variant="button" component="label"> - Tags - <Autocomplete - multiple - options={Object.keys(availableTags ?? {})} - value={selectedTags} - onChange={(_: object, value: string[]) => setSelectedTags(value)} - renderOption={(option, { selected }) => ( - <FormControlLabel - control={ - <Checkbox - icon={icon} - checkedIcon={checkedIcon} - checked={selected} - /> - } - label={ - props.showCounts - ? `${option} (${availableTags?.[option]})` - : option - } - /> - )} - size="small" - popupIcon={<ExpandMoreIcon data-testid="tag-picker-expand" />} - renderInput={params => ( - <TextField - {...params} - className={classes.input} - variant="outlined" - /> - )} - /> - </Typography> - </Box> + <EntityAutocompletePicker + label="Tags" + name="tags" + path="metadata.tags" + Filter={EntityTagFilter} + showCounts={props.showCounts} + InputProps={{ className: classes.input }} + /> ); }; diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx index f0adb878d6..09b944605f 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 } from '@backstage/catalog-model'; +import { AlphaEntity } from '@backstage/catalog-model/alpha'; import { Box, DialogContentText, diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx index 4173ca85b2..baa4b3732c 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx @@ -80,7 +80,11 @@ describe('UnregisterEntityDialog', () => { it('can cancel', async () => { const onClose = jest.fn(); - stateSpy.mockImplementation(() => ({ type: 'loading' })); + stateSpy.mockImplementation(() => ({ + type: 'bootstrap', + location: '', + deleteEntity: jest.fn(), + })); await renderInTestApp( <Wrapper> diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index 48aa2f4012..6c4984129e 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -39,14 +39,19 @@ const useStyles = makeStyles({ advancedButton: { fontSize: '0.7em', }, + dialogActions: { + display: 'inline-block', + }, }); const Contents = ({ entity, onConfirm, + onClose, }: { entity: Entity; onConfirm: () => any; + onClose: () => any; }) => { const alertApi = useApi(alertApiRef); const configApi = useApi(configApiRef); @@ -92,6 +97,14 @@ const Contents = ({ [alertApi, onConfirm, state], ); + const DialogActionsPanel = () => ( + <DialogActions className={classes.dialogActions}> + <Button onClick={onClose} color="primary"> + Cancel + </Button> + </DialogActions> + ); + if (state.type === 'loading') { return <Progress />; } @@ -112,15 +125,18 @@ const Contents = ({ <Box marginTop={2}> {!showDelete && ( - <Button - variant="text" - size="small" - color="primary" - className={classes.advancedButton} - onClick={() => setShowDelete(true)} - > - Advanced Options - </Button> + <> + <Button + variant="text" + size="small" + color="primary" + className={classes.advancedButton} + onClick={() => setShowDelete(true)} + > + Advanced Options + </Button> + <DialogActionsPanel /> + </> )} {showDelete && ( @@ -140,6 +156,7 @@ const Contents = ({ > Delete Entity </Button> + <DialogActionsPanel /> </> )} </Box> @@ -162,6 +179,7 @@ const Contents = ({ > Delete Entity </Button> + <DialogActionsPanel /> </> ); } @@ -258,13 +276,8 @@ export const UnregisterEntityDialog = (props: UnregisterEntityDialogProps) => { Are you sure you want to unregister this entity? </DialogTitle> <DialogContent> - <Contents entity={entity} onConfirm={onConfirm} /> + <Contents entity={entity} onConfirm={onConfirm} onClose={onClose} /> </DialogContent> - <DialogActions> - <Button onClick={onClose} color="primary"> - Cancel - </Button> - </DialogActions> </Dialog> ); }; diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts index c2c92e033e..2a67190633 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 { AlphaEntity, Entity } from '@backstage/catalog-model'; +import { AlphaEntity } from '@backstage/catalog-model/alpha'; +import { Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { EntityErrorFilter, diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 056b3b92c1..5bd81780d6 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -14,11 +14,8 @@ * limitations under the License. */ -import { - AlphaEntity, - Entity, - RELATION_OWNED_BY, -} from '@backstage/catalog-model'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { AlphaEntity } from '@backstage/catalog-model/alpha'; import { humanizeEntityRef } from './components/EntityRefLink'; import { EntityFilter, UserListFilterKind } from './types'; import { getEntityRelations } from './utils'; diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 3570ed897c..ffd32206ef 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -38,4 +38,3 @@ export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; export { useStarredEntity } from './useStarredEntity'; export { useEntityOwnership } from './useEntityOwnership'; -export { useEntityPermission } from './useEntityPermission'; diff --git a/plugins/catalog-react/src/hooks/useEntityPermission.test.tsx b/plugins/catalog-react/src/hooks/useEntityPermission.test.tsx index 766b587da3..aed6676838 100644 --- a/plugins/catalog-react/src/hooks/useEntityPermission.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityPermission.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common'; +import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common/alpha'; import { renderHook } from '@testing-library/react-hooks'; import { useEntityPermission } from './useEntityPermission'; import { useAsyncEntity } from './useEntity'; diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 2ca7d67d59..b511116f84 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -31,9 +31,5 @@ export { entityRouteParams, entityRouteRef } from './routes'; export * from './testUtils'; export * from './types'; export * from './overridableComponents'; -export { - getEntityRelations, - getEntitySourceLocation, - isOwnerOf, -} from './utils'; +export { getEntityRelations, getEntitySourceLocation } from './utils'; export type { EntitySourceLocation } from './utils'; diff --git a/plugins/catalog-react/src/testUtils/providers.tsx b/plugins/catalog-react/src/testUtils/providers.tsx index 5867026a8c..9bf5b21f94 100644 --- a/plugins/catalog-react/src/testUtils/providers.tsx +++ b/plugins/catalog-react/src/testUtils/providers.tsx @@ -27,26 +27,20 @@ import { } from '../hooks/useEntityListProvider'; /** @public */ -export const MockEntityListContextProvider = ({ +export function MockEntityListContextProvider< + T extends DefaultEntityFilters = DefaultEntityFilters, +>({ children, value, }: PropsWithChildren<{ - value?: Partial<EntityListContextProps>; -}>) => { + value?: Partial<EntityListContextProps<T>>; +}>) { // Provides a default implementation that stores filter state, for testing components that // reflect filter state. - const [filters, setFilters] = useState<DefaultEntityFilters>( - value?.filters ?? {}, - ); + const [filters, setFilters] = useState<T>(value?.filters ?? ({} as T)); const updateFilters = useCallback( - ( - update: - | Partial<DefaultEntityFilters> - | (( - prevFilters: DefaultEntityFilters, - ) => Partial<DefaultEntityFilters>), - ) => { + (update: Partial<T> | ((prevFilters: T) => Partial<T>)) => { setFilters(prevFilters => { const newFilters = typeof update === 'function' ? update(prevFilters) : update; @@ -67,7 +61,7 @@ export const MockEntityListContextProvider = ({ [], ); - const resolvedValue: EntityListContextProps = useMemo( + const resolvedValue: EntityListContextProps<T> = useMemo( () => ({ entities: value?.entities ?? defaultValues.entities, backendEntities: value?.backendEntities ?? defaultValues.backendEntities, @@ -85,4 +79,4 @@ export const MockEntityListContextProvider = ({ {children} </EntityListContext.Provider> ); -}; +} diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index e28bf69da0..5a4fee935a 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,113 @@ # @backstage/plugin-catalog +## 1.9.0-next.0 + +### Minor Changes + +- 3f75b7607c: Add ability to pass icon as function to have ability to customize it by search item + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## 1.8.0 + +### Minor Changes + +- 0c1fc3986c: Added Markdown support in the `AboutCard` description section +- 0eaa579f89: The `CatalogSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- d7f55f6bd2: The list of entities takes into account the title when its different from the name to sort the entities. +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- 7d46ca2f68: Make catalog search result list a bit more customizable +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.10 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-search-common@1.2.1 + +## 1.8.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + +## 1.8.0-next.1 + +### Minor Changes + +- 0c1fc3986c: Added Markdown support in the `AboutCard` description section +- 0eaa579f89: The `CatalogSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-search-common@1.2.1 + +## 1.7.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/integration-react@1.1.9 + ## 1.7.2 ### Patch Changes diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 82df851239..ee0ca180f4 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -19,6 +19,7 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; +import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react'; import { StarredEntitiesApi } from '@backstage/plugin-catalog-react'; import { StorageApi } from '@backstage/core-plugin-api'; import { StyleRules } from '@material-ui/core/styles/withStyles'; @@ -107,20 +108,20 @@ export const catalogPlugin: BackstagePlugin< >; // @public (undocumented) -export function CatalogSearchResultListItem( - props: CatalogSearchResultListItemProps, -): JSX.Element; +export const CatalogSearchResultListItem: ( + props: SearchResultListItemExtensionProps<CatalogSearchResultListItemProps>, +) => JSX.Element | null; // @public export interface CatalogSearchResultListItemProps { // (undocumented) highlight?: ResultHighlight; // (undocumented) - icon?: ReactNode; + icon?: ReactNode | ((result: IndexableDocument) => ReactNode); // (undocumented) rank?: number; // (undocumented) - result: IndexableDocument; + result?: IndexableDocument; } // @public (undocumented) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index bd4bae6437..212f4044b4 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "1.7.2", + "version": "1.9.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts index b3bc5400d5..c9083969ee 100644 --- a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts @@ -46,7 +46,7 @@ describe('performMigrationToTheNewBucket', () => { await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(await newBucket.snapshot('entityRefs').value).toEqual([ + expect(newBucket.snapshot('entityRefs').value).toEqual([ 'component:default/c', 'component:default/a', 'template:custom/b', @@ -72,7 +72,7 @@ describe('performMigrationToTheNewBucket', () => { await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(await newBucket.snapshot('entityRefs').value).toEqual([ + expect(newBucket.snapshot('entityRefs').value).toEqual([ 'component:default/a', ]); diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index 260a0eccbf..245308cee2 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -25,7 +25,8 @@ import { getEntityRelations, } from '@backstage/plugin-catalog-react'; import { JsonArray } from '@backstage/types'; -import { Chip, Grid, makeStyles, Typography } from '@material-ui/core'; +import { Chip, Grid, makeStyles } from '@material-ui/core'; +import { MarkdownContent } from '@backstage/core-components'; import React from 'react'; import { AboutField } from './AboutField'; import { LinksGridList } from '../EntityLinksCard/LinksGridList'; @@ -111,9 +112,10 @@ export function AboutContent(props: AboutContentProps) { return ( <Grid container> <AboutField label="Description" gridSizes={{ xs: 12 }}> - <Typography variant="body2" paragraph className={classes.description}> - {entity?.metadata?.description || 'No description'} - </Typography> + <MarkdownContent + className={classes.description} + content={entity?.metadata?.description || 'No description'} + /> </AboutField> <AboutField label="Owner" diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index a2b26b87e1..7b92df5809 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -34,9 +34,9 @@ import { MockStarredEntitiesApi, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; +import { MockPluginProvider } from '@backstage/test-utils/alpha'; import { mockBreakpoint, - MockPluginProvider, MockStorageApi, renderWithEffects, TestApiProvider, @@ -198,10 +198,12 @@ describe('DefaultCatalogPage', () => { it('should render the default actions of an item in the grid', async () => { await renderWrapped(<DefaultCatalogPage />); fireEvent.click(screen.getByTestId('user-picker-owned')); - expect(await screen.findByText(/Owned \(1\)/)).toBeInTheDocument(); - expect(await screen.findByTitle(/View/)).toBeInTheDocument(); - expect(await screen.findByTitle(/Edit/)).toBeInTheDocument(); - expect(await screen.findByTitle(/Add to favorites/)).toBeInTheDocument(); + await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); + await expect(screen.findByTitle(/View/)).resolves.toBeInTheDocument(); + await expect(screen.findByTitle(/Edit/)).resolves.toBeInTheDocument(); + await expect( + screen.findByTitle(/Add to favorites/), + ).resolves.toBeInTheDocument(); }, 20_000); it('should render the custom actions of an item passed as prop', async () => { @@ -226,10 +228,12 @@ describe('DefaultCatalogPage', () => { await renderWrapped(<DefaultCatalogPage actions={actions} />); fireEvent.click(screen.getByTestId('user-picker-owned')); - expect(await screen.findByText(/Owned \(1\)/)).toBeInTheDocument(); - expect(await screen.findByTitle(/Foo Action/)).toBeInTheDocument(); - expect(await screen.findByTitle(/Bar Action/)).toBeInTheDocument(); - expect((await screen.findByTitle(/Bar Action/)).firstChild).toBeDisabled(); + await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); + await expect(screen.findByTitle(/Foo Action/)).resolves.toBeInTheDocument(); + await expect(screen.findByTitle(/Bar Action/)).resolves.toBeInTheDocument(); + await expect( + screen.findByTitle(/Bar Action/).then(e => e.firstChild), + ).resolves.toBeDisabled(); }, 20_000); // this test right now causes some red lines in the log output when running tests @@ -256,7 +260,7 @@ describe('DefaultCatalogPage', () => { await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); // The "Starred" menu option should initially be disabled, since there // aren't any starred entities. - await expect(screen.getByTestId('user-picker-starred')).toHaveAttribute( + expect(screen.getByTestId('user-picker-starred')).toHaveAttribute( 'aria-disabled', 'true', ); @@ -269,7 +273,7 @@ describe('DefaultCatalogPage', () => { // Now that we've starred an entity, the "Starred" menu option should be // enabled. - await expect(screen.getByTestId('user-picker-starred')).not.toHaveAttribute( + expect(screen.getByTestId('user-picker-starred')).not.toHaveAttribute( 'aria-disabled', 'true', ); diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index 2e5cbe2899..50caf4ff8a 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -18,30 +18,33 @@ import React, { ReactNode } from 'react'; import { Box, Chip, - Divider, - ListItem, ListItemIcon, ListItemText, makeStyles, } from '@material-ui/core'; import { Link } from '@backstage/core-components'; -import { useAnalytics } from '@backstage/core-plugin-api'; import { IndexableDocument, ResultHighlight, } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; -const useStyles = makeStyles({ - flexContainer: { - flexWrap: 'wrap', +const useStyles = makeStyles( + { + item: { + display: 'flex', + }, + flexContainer: { + flexWrap: 'wrap', + }, + itemText: { + width: '100%', + wordBreak: 'break-all', + marginBottom: '1rem', + }, }, - itemText: { - width: '100%', - wordBreak: 'break-all', - marginBottom: '1rem', - }, -}); + { name: 'CatalogSearchResultListItem' }, +); /** * Props for {@link CatalogSearchResultListItem}. @@ -49,8 +52,8 @@ const useStyles = makeStyles({ * @public */ export interface CatalogSearchResultListItemProps { - icon?: ReactNode; - result: IndexableDocument; + icon?: ReactNode | ((result: IndexableDocument) => ReactNode); + result?: IndexableDocument; highlight?: ResultHighlight; rank?: number; } @@ -63,58 +66,52 @@ export function CatalogSearchResultListItem( const highlight = props.highlight as ResultHighlight; const classes = useStyles(); - const analytics = useAnalytics(); - const handleClick = () => { - analytics.captureEvent('discover', result.title, { - attributes: { to: result.location }, - value: props.rank, - }); - }; + + if (!result) return null; return ( - <> - <ListItem alignItems="flex-start"> - {props.icon && <ListItemIcon>{props.icon}</ListItemIcon>} - <div className={classes.flexContainer}> - <ListItemText - className={classes.itemText} - primaryTypographyProps={{ variant: 'h6' }} - primary={ - <Link noTrack to={result.location} onClick={handleClick}> - {highlight?.fields.title ? ( - <HighlightedSearchResultText - text={highlight.fields.title} - preTag={highlight.preTag} - postTag={highlight.postTag} - /> - ) : ( - result.title - )} - </Link> - } - secondary={ - highlight?.fields.text ? ( + <div className={classes.item}> + {props.icon && ( + <ListItemIcon> + {typeof props.icon === 'function' ? props.icon(result) : props.icon} + </ListItemIcon> + )} + <div className={classes.flexContainer}> + <ListItemText + className={classes.itemText} + primaryTypographyProps={{ variant: 'h6' }} + primary={ + <Link noTrack to={result.location}> + {highlight?.fields.title ? ( <HighlightedSearchResultText - text={highlight.fields.text} + text={highlight.fields.title} preTag={highlight.preTag} postTag={highlight.postTag} /> ) : ( - result.text - ) - } - /> - <Box> - {result.kind && ( - <Chip label={`Kind: ${result.kind}`} size="small" /> - )} - {result.lifecycle && ( - <Chip label={`Lifecycle: ${result.lifecycle}`} size="small" /> - )} - </Box> - </div> - </ListItem> - <Divider component="li" /> - </> + result.title + )} + </Link> + } + secondary={ + highlight?.fields.text ? ( + <HighlightedSearchResultText + text={highlight.fields.text} + preTag={highlight.preTag} + postTag={highlight.postTag} + /> + ) : ( + result.text + ) + } + /> + <Box> + {result.kind && <Chip label={`Kind: ${result.kind}`} size="small" />} + {result.lifecycle && ( + <Chip label={`Lifecycle: ${result.lifecycle}`} size="small" /> + )} + </Box> + </div> + </div> ); } diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 652c9f3873..f8a655413e 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -78,10 +78,9 @@ describe('CatalogTable component', () => { }, }, ); - const errorMessage = await screen.findByText( - /Could not fetch catalog entities./, - ); - expect(errorMessage).toBeInTheDocument(); + await expect( + screen.findByText(/Could not fetch catalog entities./), + ).resolves.toBeInTheDocument(); }); it('should display entity names when loading has finished and no error occurred', async () => { @@ -306,6 +305,7 @@ describe('CatalogTable component', () => { }, 20_000, ); + it('should render the subtitle when it is specified', async () => { const entity = { apiVersion: 'backstage.io/v1alpha1', diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 935e002375..18080bb7b0 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -16,6 +16,7 @@ import { ANNOTATION_EDIT_URL, ANNOTATION_VIEW_URL, + Entity, RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; @@ -62,6 +63,16 @@ const YellowStar = withStyles({ }, })(Star); +const refCompare = (a: Entity, b: Entity) => { + const toRef = (entity: Entity) => + entity.metadata.title || + humanizeEntityRef(entity, { + defaultKind: 'Component', + }); + + return toRef(a).localeCompare(toRef(b)); +}; + /** @public */ export const CatalogTable = (props: CatalogTableProps) => { const { columns, actions, tableOptions, subtitle, emptyContent } = props; @@ -177,7 +188,7 @@ export const CatalogTable = (props: CatalogTableProps) => { }, ]; - const rows = entities.map(entity => { + const rows = entities.sort(refCompare).map(entity => { const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, { kind: 'system', }); diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index d22f9bde10..675f74fa8d 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -30,8 +30,8 @@ import MoreVert from '@material-ui/icons/MoreVert'; import FileCopyTwoToneIcon from '@material-ui/icons/FileCopyTwoTone'; import React, { useCallback, useState } from 'react'; import { IconComponent } from '@backstage/core-plugin-api'; -import { useEntityPermission } from '@backstage/plugin-catalog-react'; -import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common'; +import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha'; +import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common/alpha'; import { BackstageTheme } from '@backstage/theme'; import { UnregisterEntity, UnregisterEntityOptions } from './UnregisterEntity'; import { useApi, alertApiRef } from '@backstage/core-plugin-api'; diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx index 350349fe95..b21c6292ff 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AlphaEntity, stringifyEntityRef } from '@backstage/catalog-model'; +import { AlphaEntity } from '@backstage/catalog-model/alpha'; +import { stringifyEntityRef } from '@backstage/catalog-model'; import { ApiProvider } from '@backstage/core-app-api'; import { CatalogApi, diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index 2eaf505ec0..ef91e7b26e 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -14,12 +14,8 @@ * limitations under the License. */ -import { - Entity, - AlphaEntity, - stringifyEntityRef, - EntityStatusItem, -} from '@backstage/catalog-model'; +import { AlphaEntity, EntityStatusItem } from '@backstage/catalog-model/alpha'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { catalogApiRef, EntityRefLink, diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 7de332cf51..42150dfd66 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -29,7 +29,6 @@ export type { } from './components/AboutCard'; export { AboutContent, AboutField } from './components/AboutCard'; export * from './components/CatalogKindHeader'; -export * from './components/CatalogSearchResultListItem'; export * from './components/CatalogTable'; export * from './components/EntityLayout'; export * from './components/EntityOrphanWarning'; @@ -53,6 +52,7 @@ export { EntityLinksCard, EntityLabelsCard, RelatedEntitiesCard, + CatalogSearchResultListItem, } from './plugin'; export type { DependencyOfComponentsCardProps } from './components/DependencyOfComponentsCard'; @@ -72,3 +72,4 @@ export type { HasResourcesCardProps } from './components/HasResourcesCard'; export type { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; export type { HasSystemsCardProps } from './components/HasSystemsCard'; export type { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; +export type { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; diff --git a/plugins/catalog/src/options.ts b/plugins/catalog/src/options.ts index bb248c16b6..4ea6449a4b 100644 --- a/plugins/catalog/src/options.ts +++ b/plugins/catalog/src/options.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { usePluginOptions } from '@backstage/core-plugin-api'; +import { usePluginOptions } from '@backstage/core-plugin-api/alpha'; export type CatalogPluginOptions = { createButtonTitle: string; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index c83657b85a..ea6ddcaff2 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -31,6 +31,10 @@ import { fetchApiRef, storageApiRef, } from '@backstage/core-plugin-api'; +import { + createSearchResultListItemExtension, + SearchResultListItemExtensionProps, +} from '@backstage/plugin-search-react'; import { DefaultStarredEntitiesApi } from './apis'; import { AboutCardProps } from './components/AboutCard'; import { DefaultCatalogPageProps } from './components/CatalogPage'; @@ -42,6 +46,7 @@ import { HasResourcesCardProps } from './components/HasResourcesCard'; import { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; import { HasSystemsCardProps } from './components/HasSystemsCard'; import { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; +import { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; import { rootRouteRef } from './routes'; import { CatalogInputPluginOptions, CatalogPluginOptions } from './options'; @@ -249,3 +254,17 @@ export const RelatedEntitiesCard: <T extends Entity>( }, }), ); + +/** @public */ +export const CatalogSearchResultListItem: ( + props: SearchResultListItemExtensionProps<CatalogSearchResultListItemProps>, +) => JSX.Element | null = catalogPlugin.provide( + createSearchResultListItemExtension({ + name: 'CatalogSearchResultListItem', + component: () => + import('./components/CatalogSearchResultListItem').then( + m => m.CatalogSearchResultListItem, + ), + predicate: result => result.type === 'software-catalog', + }), +); diff --git a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md index 1fb34871d6..bd49967ff4 100644 --- a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md +++ b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-cicd-statistics-module-gitlab +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-cicd-statistics@0.1.18-next.0 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cicd-statistics@0.1.17 + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + +## 0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/plugin-cicd-statistics@0.1.17-next.2 + +## 0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cicd-statistics@0.1.17-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + +## 0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-cicd-statistics@0.1.17-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json index 70a9d630ea..7db1fa63f5 100644 --- a/plugins/cicd-statistics-module-gitlab/package.json +++ b/plugins/cicd-statistics-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics-module-gitlab", "description": "CI/CD Statistics plugin module; Gitlab CICD", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md index 22d5e67a36..e60210b30b 100644 --- a/plugins/cicd-statistics/CHANGELOG.md +++ b/plugins/cicd-statistics/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-cicd-statistics +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + +## 0.1.17 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.17-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + +## 0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index 3bfd432a01..79469a7ce8 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.16", + "version": "0.1.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -41,7 +41,7 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@date-io/luxon": "^1.3.13", - "@material-ui/core": "^4.9.13", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/pickers": "^3.3.10", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 6bfcd0373e..43d0b72410 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-circleci +## 0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.3.15 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- 1616e06047: Making workflow a link +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.3.15-next.2 + +### Patch Changes + +- 1616e06047: Making workflow a link +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.3.15-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 4bd346c96e..b657d07f28 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.3.14", + "version": "0.3.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -62,7 +62,7 @@ "@types/humanize-duration": "^3.25.1", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx index 8d48be915e..8a5dc1c47f 100644 --- a/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -24,7 +24,7 @@ import LaunchIcon from '@material-ui/icons/Launch'; import { useBuildWithSteps } from '../../state/useBuildWithSteps'; import { Breadcrumbs, - Button, + LinkButton, InfoCard, Progress, Link, @@ -33,9 +33,9 @@ import { const BuildName = ({ build }: { build?: BuildWithSteps }) => ( <Box display="flex" alignItems="center"> #{build?.build_num} - {build?.subject} - <Button to={build?.build_url || '#'}> + <LinkButton to={build?.build_url || '#'}> <LaunchIcon /> - </Button> + </LinkButton> </Box> ); diff --git a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx index adc8a569bb..22f69ac30f 100644 --- a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -212,6 +212,14 @@ const generatedColumns: TableColumn[] = [ { title: 'Workflow', field: 'workflow.name', + highlight: true, + render: (row: Partial<CITableBuildInfo>) => ( + <Link + to={`https://app.circleci.com/pipelines/workflows/${row?.workflow?.id}`} + > + {row?.workflow?.name} + </Link> + ), }, { title: 'Actions', diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts index 760c57d764..1e199cfd34 100644 --- a/plugins/circleci/src/state/useBuilds.ts +++ b/plugins/circleci/src/state/useBuilds.ts @@ -64,7 +64,7 @@ const mapSourceDetails = (buildData: BuildSummary) => { branchName: String(buildData.branch), commit: { hash: String(buildData.vcs_revision), - shortHash: String(buildData.vcs_revision).substr(0, 7), + shortHash: String(buildData.vcs_revision).slice(0, 7), committerName: buildData.committer_name, url: commitDetails.commit_url, }, diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index c78ea57cdc..7939ee1ad4 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/plugin-cloudbuild +## 0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 6683982abe..7604fdaf63 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cloudbuild", "description": "A Backstage plugin that integrates towards Google Cloud Build", - "version": "0.3.14", + "version": "0.3.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -59,7 +59,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md index 5e6e58bd54..5d4e998846 100644 --- a/plugins/code-climate/CHANGELOG.md +++ b/plugins/code-climate/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/plugin-code-climate +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.14 ### Patch Changes diff --git a/plugins/code-climate/dev/index.tsx b/plugins/code-climate/dev/index.tsx index d5fc4c28e3..060679337e 100644 --- a/plugins/code-climate/dev/index.tsx +++ b/plugins/code-climate/dev/index.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import { createDevApp, EntityGridItem } from '@backstage/dev-utils'; import { Grid } from '@material-ui/core'; import React from 'react'; diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index 5567f8b048..0f99afff6a 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-climate", - "version": "0.1.14", + "version": "0.1.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -49,7 +49,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/code-climate/src/setupTests.ts b/plugins/code-climate/src/setupTests.ts index 43d9e09713..eb4654b869 100644 --- a/plugins/code-climate/src/setupTests.ts +++ b/plugins/code-climate/src/setupTests.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ 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 fc1bbe4d00..e6606e3eea 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-code-coverage-backend +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.2.8 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + ## 0.2.6 ### Patch Changes diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md index ae6021b07c..802e538125 100644 --- a/plugins/code-coverage-backend/README.md +++ b/plugins/code-coverage-backend/README.md @@ -4,7 +4,10 @@ This is the backend part of the `code-coverage` plugin. It takes care of process ## Installation -`yarn workspace backend add @backstage/plugin-code-coverage-backend` +```sh +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-code-coverage-backend +``` First create a `codecoverage.ts` file here: `packages/backend/src/plugins`. Now add the following as its content: diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 908ab1f978..bb0c7df590 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.2.6", + "version": "0.2.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,7 +42,7 @@ "@backstage/cli": "workspace:^", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.6", "xml2js": "^0.4.23" }, diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index dbcbb6fdbe..36c7617431 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-code-coverage +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.2.8 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.2.7 ### Patch Changes diff --git a/plugins/code-coverage/README.md b/plugins/code-coverage/README.md index ce793a78d3..d4daa6ec00 100644 --- a/plugins/code-coverage/README.md +++ b/plugins/code-coverage/README.md @@ -4,7 +4,10 @@ This is the frontend part of the code-coverage plugin. It displays code coverage ## Installation -`yarn workspace app add @backstage/plugin-code-coverage` +```sh +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-code-coverage +``` Finally you need to import and render the code coverage entity, in `packages/app/src/components/catalog/EntityPage.tsx` add the following: diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 5c181d0a8c..3a2de06c7f 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.2.7", + "version": "0.2.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -55,7 +55,7 @@ "@types/node": "^16.11.26", "@types/recharts": "^1.8.15", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/codescene/CHANGELOG.md b/plugins/codescene/CHANGELOG.md index deb3feb593..e8ab4d63cf 100644 --- a/plugins/codescene/CHANGELOG.md +++ b/plugins/codescene/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-codescene +## 0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.1.10 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.1.10-next.0 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + ## 0.1.9 ### Patch Changes diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index 8d2e91b0fd..23af32e94a 100644 --- a/plugins/codescene/package.json +++ b/plugins/codescene/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-codescene", - "version": "0.1.9", + "version": "0.1.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,7 +27,7 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.9.10", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.57", "rc-progress": "3.4.1", @@ -47,7 +47,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 0c9889e2ae..5a52322ec0 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-config-schema +## 0.1.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + +## 0.1.38 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.1.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## 0.1.38-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + ## 0.1.37 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 436a2c8ca5..07773496c0 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.37", + "version": "0.1.39-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -49,7 +49,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 343319032a..e94efa2602 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-cost-insights +## 0.12.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-cost-insights-common@0.1.1 + +## 0.12.4 + +### Patch Changes + +- dfa0bb737e: Fix bug in EntityCostsCard if cost aggregation array is empty. Allow title override on the Cost Insights Page +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + - @backstage/plugin-cost-insights-common@0.1.1 + +## 0.12.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-cost-insights-common@0.1.1 + +## 0.12.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + +## 0.12.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.12.3 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 1a3ced06a7..bce00a354f 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.12.3", + "version": "0.12.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -74,7 +74,7 @@ "@types/yup": "^0.29.13", "canvas": "^2.10.2", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx index 0a583aae0d..55d5668189 100644 --- a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx +++ b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx @@ -37,6 +37,7 @@ type CostInsightsLayoutProps = { }; export const CostInsightsLayout = ({ + title = 'Cost Insights', groups, children, }: PropsWithChildren<CostInsightsLayoutProps>) => { @@ -46,7 +47,7 @@ export const CostInsightsLayout = ({ <Header style={{ boxShadow: 'none' }} title="Cost Insights" - pageTitleOverride="Cost Insights" + pageTitleOverride={title} type="Tool" /> <div className={classes.root}> diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx index 0a9451c794..d4fdc66bdb 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx @@ -15,7 +15,8 @@ */ import React from 'react'; import { fireEvent } from '@testing-library/react'; -import { MockPluginProvider, renderInTestApp } from '@backstage/test-utils'; +import { MockPluginProvider } from '@backstage/test-utils/alpha'; +import { renderInTestApp } from '@backstage/test-utils'; import { CostOverviewCard } from './CostOverviewCard'; import { Cost } from '@backstage/plugin-cost-insights-common'; import { diff --git a/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx b/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx index f288f54e9b..6eb3369b40 100644 --- a/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx +++ b/plugins/cost-insights/src/components/EntityCosts/EntityCost.test.tsx @@ -14,11 +14,8 @@ * limitations under the License. */ import React from 'react'; -import { - MockPluginProvider, - renderInTestApp, - TestApiProvider, -} from '@backstage/test-utils'; +import { MockPluginProvider } from '@backstage/test-utils/alpha'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { changeOf, MockAggregatedDailyCosts, diff --git a/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx index 9dda1c65e3..69bf8885e7 100644 --- a/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx +++ b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx @@ -120,7 +120,7 @@ export const EntityCostsCard = () => { return <MaterialAlert severity="error">{error.message}</MaterialAlert>; } - if (!dailyCost) { + if (!dailyCost || !dailyCost.aggregation.length) { return <MaterialAlert severity="error">No daily costs</MaterialAlert>; } diff --git a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts index 50c3bb1470..046196f26b 100644 --- a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts +++ b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts @@ -22,7 +22,7 @@ // IMPORTANT: Remove the lines below to enable type checking and linting // @ts-nocheck -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ import { CostInsightsApi, diff --git a/plugins/cost-insights/src/options.ts b/plugins/cost-insights/src/options.ts index 287935d703..acc977771d 100644 --- a/plugins/cost-insights/src/options.ts +++ b/plugins/cost-insights/src/options.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { usePluginOptions } from '@backstage/core-plugin-api'; +import { usePluginOptions } from '@backstage/core-plugin-api/alpha'; export type CostInsightsPluginOptions = { hideTrendLine?: boolean; diff --git a/plugins/dynatrace/CHANGELOG.md b/plugins/dynatrace/CHANGELOG.md index 63ffdd5da9..1daec214f0 100644 --- a/plugins/dynatrace/CHANGELOG.md +++ b/plugins/dynatrace/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-dynatrace +## 3.0.0-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 2.0.0 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 2.0.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 2.0.0-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 2.0.0-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 1.0.4 ### Patch Changes diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json index 03c0f97f16..ef8c2bb22b 100644 --- a/plugins/dynatrace/package.json +++ b/plugins/dynatrace/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-dynatrace", - "version": "1.0.4", + "version": "3.0.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.9.13", + "@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" @@ -50,7 +50,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "express": "^4.18.1", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/entity-feedback-backend/.eslintrc.js b/plugins/entity-feedback-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/entity-feedback-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/entity-feedback-backend/CHANGELOG.md b/plugins/entity-feedback-backend/CHANGELOG.md new file mode 100644 index 0000000000..86f7debc5c --- /dev/null +++ b/plugins/entity-feedback-backend/CHANGELOG.md @@ -0,0 +1,29 @@ +# @backstage/plugin-entity-feedback-backend + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-entity-feedback-common@0.1.0 + +## 0.1.0 + +### Minor Changes + +- a3c86a7ed2: Implement entity feedback plugin, check out the `README.md` for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-entity-feedback-common@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/plugin-auth-node@0.2.11 diff --git a/plugins/entity-feedback-backend/README.md b/plugins/entity-feedback-backend/README.md new file mode 100644 index 0000000000..ef180dbae5 --- /dev/null +++ b/plugins/entity-feedback-backend/README.md @@ -0,0 +1,50 @@ +# Entity Feedback Backend + +Welcome to the entity-feedback backend plugin! + +## Installation + +### Install the package + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-entity-feedback-backend +``` + +### Adding the plugin to your `packages/backend` + +You'll need to add the plugin to the router in your `backend` package. You can do this by creating a file called `packages/backend/src/plugins/entityFeedback.ts` + +```tsx +import { createRouter } from '@backstage/plugin-entity-feedback-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default function createPlugin(env: PluginEnvironment): Promise<Router> { + return createRouter({ + database: env.database, + discovery: env.discovery, + identity: env.identity, + logger: env.logger, + }); +} +``` + +With the `entityFeedback.ts` router setup in place, add the router to `packages/backend/src/index.ts`: + +```diff ++import entityFeedback from './plugins/entityFeedback'; + +async function main() { + ... + const createEnv = makeCreateEnv(config); + + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); ++ const entityFeedbackEnv = useHotMemoize(module, () => createEnv('entityFeedback')); + + const apiRouter = Router(); ++ apiRouter.use('/entity-feedback', await entityFeedback(entityFeedbackEnv)); + ... + apiRouter.use(notFoundHandler()); + +``` diff --git a/plugins/entity-feedback-backend/api-report.md b/plugins/entity-feedback-backend/api-report.md new file mode 100644 index 0000000000..3909ac3056 --- /dev/null +++ b/plugins/entity-feedback-backend/api-report.md @@ -0,0 +1,26 @@ +## API Report File for "@backstage/plugin-entity-feedback-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import express from 'express'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { Logger } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise<express.Router>; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + database: PluginDatabaseManager; + // (undocumented) + discovery: PluginEndpointDiscovery; + // (undocumented) + identity: IdentityApi; + // (undocumented) + logger: Logger; +} +``` diff --git a/plugins/entity-feedback-backend/migrations/20230109124329_init.js b/plugins/entity-feedback-backend/migrations/20230109124329_init.js new file mode 100644 index 0000000000..42a48d264c --- /dev/null +++ b/plugins/entity-feedback-backend/migrations/20230109124329_init.js @@ -0,0 +1,69 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param { import("knex").Knex } knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('ratings', table => { + table.comment('Ratings table'); + table + .string('entity_ref') + .notNullable() + .comment('The ref of the entity being rated'); + table + .string('user_ref') + .notNullable() + .comment('The user applying the rating'); + table.string('rating').notNullable().comment('The rating value'); + table + .timestamp('timestamp') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('When the rating was recorded'); + }); + + await knex.schema.createTable('responses', table => { + table.comment('Responses table'); + table + .string('entity_ref') + .notNullable() + .comment('The ref of the applicable entity'); + table.string('user_ref').notNullable().comment('The user responding'); + table.text('response').comment('The serialized response'); + table.text('comments').comment('Additional user comments'); + table + .boolean('consent') + .defaultTo(true) + .notNullable() + .comment('Whether user (if recorded) consents to being contacted'); + table + .timestamp('timestamp') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('When the response was recorded'); + }); +}; + +/** + * @param { import("knex").Knex } knex + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('ratings'); + await knex.schema.dropTable('responses'); +}; diff --git a/plugins/entity-feedback-backend/package.json b/plugins/entity-feedback-backend/package.json new file mode 100644 index 0000000000..b7951e1db9 --- /dev/null +++ b/plugins/entity-feedback-backend/package.json @@ -0,0 +1,50 @@ +{ + "name": "@backstage/plugin-entity-feedback-backend", + "version": "0.1.1-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-entity-feedback-common": "workspace:^", + "@types/express": "*", + "express": "^4.18.1", + "express-promise-router": "^4.1.0", + "knex": "^2.0.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.12", + "msw": "^1.0.0", + "supertest": "^6.2.4" + }, + "files": [ + "dist", + "migrations/**/*.{js,d.ts}" + ] +} diff --git a/plugins/entity-feedback-backend/src/index.ts b/plugins/entity-feedback-backend/src/index.ts new file mode 100644 index 0000000000..b3c4139eed --- /dev/null +++ b/plugins/entity-feedback-backend/src/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Entity Feedback backend plugin + * + * @packageDocumentation + */ +export * from './service/router'; diff --git a/plugins/entity-feedback-backend/src/run.ts b/plugins/entity-feedback-backend/src/run.ts new file mode 100644 index 0000000000..7344f24c17 --- /dev/null +++ b/plugins/entity-feedback-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { 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/entity-feedback-backend/src/service/DatabaseHandler.test.ts b/plugins/entity-feedback-backend/src/service/DatabaseHandler.test.ts new file mode 100644 index 0000000000..c6cde95b3c --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/DatabaseHandler.test.ts @@ -0,0 +1,310 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Knex } from 'knex'; + +import { DatabaseHandler } from './DatabaseHandler'; + +describe('DatabaseHandler', () => { + const databases = TestDatabases.create(); + + async function createDatabaseHandler(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + return { + knex, + dbHandler: await DatabaseHandler.create({ database: knex }), + }; + } + + describe.each(databases.eachSupportedId())( + '%p', + databaseId => { + let knex: Knex; + let dbHandler: DatabaseHandler; + + beforeEach(async () => { + ({ knex, dbHandler } = await createDatabaseHandler(databaseId)); + }, 30000); + + afterEach(async () => { + // Clean up after each test + await knex('ratings').del(); + await knex('responses').del(); + }, 30000); + + it('getAllRatedEntities', async () => { + await knex('ratings').insert([ + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'DISLIKE', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'LIKE', + }, + { + entity_ref: 'component:default/library', + user_ref: 'user:default/bar', + rating: 'LIKE', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/foo', + rating: 'LIKE', + }, + ]); + + const entities = await dbHandler.getAllRatedEntities(); + expect(entities.length).toEqual(3); + expect(entities).toEqual( + expect.arrayContaining([ + 'component:default/service', + 'component:default/library', + 'component:default/website', + ]), + ); + }); + + it('getRatingsAggregates', async () => { + await knex('ratings').insert([ + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'DISLIKE', + timestamp: '2004-10-19 10:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'LIKE', + timestamp: '2004-11-19 08:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/bar', + rating: 'LIKE', + timestamp: '2004-11-20 08:23:54', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/foo', + rating: 'LIKE', + timestamp: '2004-11-20 08:23:54', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/test', + rating: 'DISLIKE', + timestamp: '2004-11-20 08:23:54', + }, + ]); + + let ratings = await dbHandler.getRatingsAggregates([ + 'component:default/service', + 'component:default/website', + ]); + expect(ratings.length).toEqual(3); + expect(ratings).toEqual( + expect.arrayContaining([ + { + entityRef: 'component:default/service', + rating: 'LIKE', + count: 2, + }, + { + entityRef: 'component:default/website', + rating: 'DISLIKE', + count: 1, + }, + { + entityRef: 'component:default/website', + rating: 'LIKE', + count: 1, + }, + ]), + ); + + ratings = await dbHandler.getRatingsAggregates([ + 'component:default/website', + ]); + expect(ratings.length).toEqual(2); + expect(ratings).toEqual( + expect.arrayContaining([ + { + entityRef: 'component:default/website', + rating: 'DISLIKE', + count: 1, + }, + { + entityRef: 'component:default/website', + rating: 'LIKE', + count: 1, + }, + ]), + ); + }); + + it('recordRating', async () => { + const newRating = { + userRef: 'user:default/me', + entityRef: 'component:default/service', + rating: 'LIKE', + }; + + await dbHandler.recordRating(newRating); + + const ratings = await knex('ratings').select(); + expect(ratings.length).toEqual(1); + expect(ratings[0]).toEqual( + expect.objectContaining({ + user_ref: newRating.userRef, + entity_ref: newRating.entityRef, + rating: newRating.rating, + timestamp: expect.anything(), + }), + ); + }); + + it('getRatings', async () => { + await knex('ratings').insert([ + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'DISLIKE', + timestamp: '2004-10-19 10:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'LIKE', + timestamp: '2004-11-19 08:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/bar', + rating: 'LIKE', + timestamp: '2004-11-20 08:23:54', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/foo', + rating: 'LIKE', + timestamp: '2004-11-20 08:23:54', + }, + ]); + + const ratings = await dbHandler.getRatings('component:default/service'); + expect(ratings.length).toEqual(2); + expect(ratings).toEqual( + expect.arrayContaining([ + { userRef: 'user:default/foo', rating: 'LIKE' }, + { userRef: 'user:default/bar', rating: 'LIKE' }, + ]), + ); + }); + + it('recordResponse', async () => { + const newResponse = { + userRef: 'user:default/me', + entityRef: 'component:default/service', + response: 'blah', + comments: 'here is some feedback', + consent: false, + }; + + await dbHandler.recordResponse(newResponse); + + const responses = await knex('responses').select(); + expect(responses.length).toEqual(1); + expect({ + ...responses[0], + consent: Boolean(responses[0].consent), + }).toEqual( + expect.objectContaining({ + user_ref: newResponse.userRef, + entity_ref: newResponse.entityRef, + response: newResponse.response, + comments: newResponse.comments, + consent: newResponse.consent, + timestamp: expect.anything(), + }), + ); + }); + + it('getResponses', async () => { + await knex('responses').insert([ + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + response: 'blah', + comments: 'here is some feedback', + consent: true, + timestamp: '2004-10-19 10:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + response: 'asdf', + comments: 'here is new feedback', + consent: false, + timestamp: '2004-11-19 08:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/bar', + response: 'noop', + comments: 'here is different feedback', + consent: true, + timestamp: '2004-11-20 08:23:54', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/foo', + response: 'cool', + comments: 'no comment', + consent: true, + timestamp: '2004-11-20 08:23:54', + }, + ]); + + const responses = await dbHandler.getResponses( + 'component:default/service', + ); + expect(responses.length).toEqual(2); + expect(responses).toEqual( + expect.arrayContaining([ + { + userRef: 'user:default/foo', + response: 'asdf', + comments: 'here is new feedback', + consent: false, + }, + { + userRef: 'user:default/bar', + response: 'noop', + comments: 'here is different feedback', + consent: true, + }, + ]), + ); + }); + }, + 60000, + ); +}); diff --git a/plugins/entity-feedback-backend/src/service/DatabaseHandler.ts b/plugins/entity-feedback-backend/src/service/DatabaseHandler.ts new file mode 100644 index 0000000000..9b81cd94bb --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/DatabaseHandler.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolvePackagePath } from '@backstage/backend-common'; +import { + FeedbackResponse, + Rating, +} from '@backstage/plugin-entity-feedback-common'; +import { Knex } from 'knex'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-entity-feedback-backend', + 'migrations', +); + +type RatingsAggregatesResult = { + entityRef: string; + rating: string; + count: number; +}[]; + +type Options = { + database: Knex; +}; + +/** + * @public + */ +export class DatabaseHandler { + static async create(options: Options): Promise<DatabaseHandler> { + const { database } = options; + + await database.migrate.latest({ + directory: migrationsDir, + }); + + return new DatabaseHandler(options); + } + + private readonly database: Knex; + + private constructor(options: Options) { + this.database = options.database; + } + + async getAllRatedEntities(): Promise<string[]> { + return (await this.database('ratings').distinct('entity_ref')).map( + ({ entity_ref }) => entity_ref, + ); + } + + async getRatingsAggregates( + entityRefs: string[], + ): Promise<RatingsAggregatesResult> { + return ( + await this.database('ratings') + .whereIn('ratings.entity_ref', entityRefs) + .innerJoin( + this.database('ratings') + .as('latest_ratings') + .select('entity_ref', 'user_ref') + .max('timestamp', { as: 'timestamp' }) + .whereIn('entity_ref', entityRefs) + .groupBy('entity_ref', 'user_ref'), + function joinClause() { + this.on('ratings.entity_ref', '=', 'latest_ratings.entity_ref') + .andOn('ratings.user_ref', '=', 'latest_ratings.user_ref') + .andOn('ratings.timestamp', '=', 'latest_ratings.timestamp'); + }, + ) + .select('ratings.entity_ref', 'rating') + .count('ratings.user_ref as count') + .groupBy('ratings.entity_ref', 'rating') + ).map(({ entity_ref, rating, count }) => ({ + entityRef: entity_ref as string, + rating: rating as string, + count: Number(count), + })); + } + + async recordRating(rating: Rating) { + await this.database('ratings').insert({ + entity_ref: rating.entityRef, + rating: rating.rating, + user_ref: rating.userRef, + }); + } + + async getRatings(entityRef: string): Promise<Omit<Rating, 'entityRef'>[]> { + return ( + await this.database('ratings') + .where('entity_ref', entityRef) + .innerJoin( + this.database('ratings') + .as('latest_ratings') + .select('user_ref') + .max('timestamp', { as: 'timestamp' }) + .where('entity_ref', entityRef) + .groupBy('user_ref'), + function joinClause() { + this.on('ratings.user_ref', '=', 'latest_ratings.user_ref').andOn( + 'ratings.timestamp', + '=', + 'latest_ratings.timestamp', + ); + }, + ) + .select('ratings.user_ref', 'rating') + ).map(rating => ({ userRef: rating.user_ref, rating: rating.rating })); + } + + async recordResponse(response: FeedbackResponse) { + await this.database('responses').insert({ + entity_ref: response.entityRef, + response: response.response, + comments: response.comments, + consent: response.consent, + user_ref: response.userRef, + }); + } + + async getResponses( + entityRef: string, + ): Promise<Omit<FeedbackResponse, 'entityRef'>[]> { + return ( + await this.database('responses') + .where('entity_ref', entityRef) + .innerJoin( + this.database('responses') + .as('latest_responses') + .select('user_ref') + .max('timestamp', { as: 'timestamp' }) + .where('entity_ref', entityRef) + .groupBy('user_ref'), + function joinClause() { + this.on( + 'responses.user_ref', + '=', + 'latest_responses.user_ref', + ).andOn('responses.timestamp', '=', 'latest_responses.timestamp'); + }, + ) + .select('responses.user_ref', 'response', 'comments', 'consent') + ).map(response => ({ + userRef: response.user_ref, + response: response.response, + comments: response.comments, + consent: Boolean(response.consent), + })); + } +} diff --git a/plugins/entity-feedback-backend/src/service/router.test.ts b/plugins/entity-feedback-backend/src/service/router.test.ts new file mode 100644 index 0000000000..74fae8248b --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/router.test.ts @@ -0,0 +1,290 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DatabaseManager, + getVoidLogger, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +const sampleOwnedEntities = [ + { + kind: 'component', + metadata: { + namespace: 'default', + name: 'foo', + title: 'Foo Component', + }, + }, + { + kind: 'component', + metadata: { + namespace: 'default', + name: 'bar', + title: 'Bar Component', + }, + }, +]; + +const sampleEntities = [ + ...sampleOwnedEntities, + { + kind: 'user', + metadata: { + namespace: 'default', + name: 'foo', + }, + }, + null, + { + kind: 'user', + metadata: { + namespace: 'default', + name: 'bar', + }, + }, +]; + +const mockGetEntties = jest + .fn() + .mockImplementation(async () => ({ items: sampleOwnedEntities })); + +const mockGetEnttiesByRefs = jest + .fn() + .mockImplementation(async () => ({ items: sampleEntities })); + +jest.mock('@backstage/catalog-client', () => ({ + CatalogClient: jest.fn().mockImplementation(() => ({ + getEntities: mockGetEntties, + getEntitiesByRefs: mockGetEnttiesByRefs, + })), +})); + +jest.mock('@backstage/plugin-auth-node', () => ({ + getBearerTokenFromAuthorizationHeader: () => 'token', +})); + +const mockRatings = [ + { userRef: 'user:default/foo', rating: 'LIKE' }, + { userRef: 'user:default/bar', rating: 'LIKE' }, + { userRef: 'user:default/test', rating: 'DISLIKE' }, +]; + +const mockResponses = [ + { + userRef: 'user:default/foo', + response: 'asdf', + comments: 'here is new feedback', + consent: false, + }, + { + userRef: 'user:default/bar', + response: 'noop', + comments: 'here is different feedback', + consent: true, + }, + { + userRef: 'user:default/test', + response: 'err', + comments: 'no comment', + consent: false, + }, +]; + +const mockDbHandler = { + getAllRatedEntities: jest + .fn() + .mockImplementation(async () => [ + 'component:default/foo', + 'component:default/bar', + 'component:default/test', + ]), + getRatingsAggregates: jest.fn().mockImplementation(async () => [ + { entityRef: 'component:default/foo', rating: 'LIKE', count: 3 }, + { entityRef: 'component:default/foo', rating: 'DISLIKE', count: 1 }, + { entityRef: 'component:default/bar', rating: 'LIKE', count: 5 }, + ]), + recordRating: jest.fn().mockImplementation(async () => {}), + getRatings: jest.fn().mockImplementation(async () => mockRatings), + recordResponse: jest.fn().mockImplementation(async () => {}), + getResponses: jest.fn().mockImplementation(async () => mockResponses), +}; + +jest.mock('./DatabaseHandler', () => ({ + DatabaseHandler: { create: async () => mockDbHandler }, +})); + +describe('createRouter', () => { + let app: express.Express; + + const createDatabase = () => + DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('entity-feedback'); + + const mockIdentityClient = { + getIdentity: jest.fn().mockImplementation(async () => ({ + identity: { userEntityRef: 'user:default/me' }, + })), + } as unknown as IdentityApi; + + const discovery: jest.Mocked<PluginEndpointDiscovery> = { + getBaseUrl: jest.fn(), + getExternalBaseUrl: jest.fn(), + }; + + beforeEach(async () => { + const router = await createRouter({ + database: createDatabase(), + discovery, + identity: mockIdentityClient, + logger: getVoidLogger(), + }); + + app = express().use(router); + jest.clearAllMocks(); + }); + + describe('GET /ratings', () => { + it('should get ratings for all entities correctly', async () => { + const response = await request(app).get('/ratings').send(); + + expect(mockDbHandler.getAllRatedEntities).toHaveBeenCalled(); + expect(mockDbHandler.getRatingsAggregates).toHaveBeenCalledWith( + sampleEntities + .filter(Boolean) + .map((ent: any) => stringifyEntityRef(ent)), + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo Component', + ratings: { LIKE: 3, DISLIKE: 1 }, + }, + { + entityRef: 'component:default/bar', + entityTitle: 'Bar Component', + ratings: { LIKE: 5 }, + }, + ]); + }); + + it('should get ratings for all owned entities correctly', async () => { + const response = await request(app) + .get('/ratings?ownerRef=group:default/test-team') + .send(); + + expect(mockGetEntties).toHaveBeenCalledWith( + expect.objectContaining({ + filter: { 'relations.ownedBy': 'group:default/test-team' }, + }), + { token: 'token' }, + ); + expect(mockDbHandler.getAllRatedEntities).not.toHaveBeenCalled(); + expect(mockDbHandler.getRatingsAggregates).toHaveBeenCalledWith( + sampleOwnedEntities.map((ent: any) => stringifyEntityRef(ent)), + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo Component', + ratings: { LIKE: 3, DISLIKE: 1 }, + }, + { + entityRef: 'component:default/bar', + entityTitle: 'Bar Component', + ratings: { LIKE: 5 }, + }, + ]); + }); + }); + + describe('POST /ratings/:entityRef', () => { + it('should record a rating correctly', async () => { + const body = { rating: 'LIKE' }; + const response = await request(app) + .post('/ratings/component%3Adefault%2Fservice') + .send(body); + expect(mockDbHandler.recordRating).toHaveBeenCalledWith({ + entityRef: 'component:default/service', + userRef: 'user:default/me', + ...body, + }); + expect(response.status).toEqual(201); + }); + }); + + describe('GET /ratings/:entityRef', () => { + it('should get ratings for an entity correctly', async () => { + const response = await request(app) + .get('/ratings/component%3Adefault%2Fservice') + .send(); + expect(mockDbHandler.getRatings).toHaveBeenCalledWith( + 'component:default/service', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual( + mockRatings.filter(r => r.userRef !== 'user:default/test'), + ); + }); + }); + + describe('POST /responses/:entityRef', () => { + it('should record a response correctly', async () => { + const body = { response: 'blah', comments: 'feedback', consent: true }; + const response = await request(app) + .post('/responses/component%3Adefault%2Fservice') + .send(body); + expect(mockDbHandler.recordResponse).toHaveBeenCalledWith({ + entityRef: 'component:default/service', + userRef: 'user:default/me', + ...body, + }); + expect(response.status).toEqual(201); + }); + }); + + describe('GET /responses/:entityRef', () => { + it('should get responses for an entity correctly', async () => { + const response = await request(app) + .get('/responses/component%3Adefault%2Fservice') + .send(); + expect(mockDbHandler.getResponses).toHaveBeenCalledWith( + 'component:default/service', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual( + mockResponses.filter(r => r.userRef !== 'user:default/test'), + ); + }); + }); +}); diff --git a/plugins/entity-feedback-backend/src/service/router.ts b/plugins/entity-feedback-backend/src/service/router.ts new file mode 100644 index 0000000000..e1243cedaf --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/router.ts @@ -0,0 +1,215 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + errorHandler, + PluginDatabaseManager, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { + getBearerTokenFromAuthorizationHeader, + IdentityApi, +} from '@backstage/plugin-auth-node'; +import { EntityRatingsData } from '@backstage/plugin-entity-feedback-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +import { DatabaseHandler } from './DatabaseHandler'; + +/** + * @public + */ +export interface RouterOptions { + database: PluginDatabaseManager; + discovery: PluginEndpointDiscovery; + identity: IdentityApi; + logger: Logger; +} + +/** + * @public + */ +export async function createRouter( + options: RouterOptions, +): Promise<express.Router> { + const { database, discovery, identity, logger } = options; + + logger.info('Initializing Entity Feedback backend'); + + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const db = await database.getClient(); + const dbHandler = await DatabaseHandler.create({ database: db }); + + const router = Router(); + router.use(express.json()); + + router.get('/ratings', async (req, res) => { + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + const requestedEntities: { [ref: string]: Entity } = {}; + if (req.query.ownerRef) { + // Get ratings from all owned entities (also ensures only accessible entities are requested) + ( + await catalogClient.getEntities( + { + filter: { 'relations.ownedBy': req.query.ownerRef as string }, + fields: [ + 'kind', + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + ], + }, + { token }, + ) + ).items.forEach(ent => { + requestedEntities[stringifyEntityRef(ent)] = ent; + }); + } else { + const allRatedEntities = await dbHandler.getAllRatedEntities(); + + // Filter entities to only expose entity refs accessible by current user + ( + await catalogClient.getEntitiesByRefs( + { + entityRefs: allRatedEntities, + fields: [ + 'kind', + 'metadata.namespace', + 'metadata.name', + 'metadata.title', + ], + }, + { token }, + ) + ).items + .filter(Boolean) + .forEach(ent => { + requestedEntities[stringifyEntityRef(ent!)] = ent!; + }); + } + + const entityRatings = await dbHandler.getRatingsAggregates( + Object.keys(requestedEntities), + ); + + // Merge rating aggregates into a condensed per entity structure + const entityRatingsMap = entityRatings.reduce<{ + [ref: string]: EntityRatingsData; + }>((ratingsMap, { entityRef, rating, count }) => { + ratingsMap[entityRef] = ratingsMap[entityRef] ?? { + entityRef, + entityTitle: requestedEntities[entityRef].metadata.title, + ratings: {}, + }; + ratingsMap[entityRef].ratings[rating] = count; + return ratingsMap; + }, {}); + + res.json(Object.values(entityRatingsMap)); + }); + + router.post('/ratings/:entityRef', async (req, res) => { + const user = await identity.getIdentity({ request: req }); + const rating = req.body.rating; + if (!user || !rating) { + res.status(400).end(); + return; + } + + await dbHandler.recordRating({ + entityRef: req.params.entityRef, + rating, + userRef: user.identity.userEntityRef, + }); + + res.status(201).end(); + }); + + router.get('/ratings/:entityRef', async (req, res) => { + const ratings = await dbHandler.getRatings(req.params.entityRef); + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + // Filter ratings via user refs to only expose entity refs accessible by current user + const accessibleEntityRefs = ( + await catalogClient.getEntitiesByRefs( + { + entityRefs: ratings.map(r => r.userRef), + fields: ['kind', 'metadata.namespace', 'metadata.name'], + }, + { token }, + ) + ).items + .filter(Boolean) + .map(ent => stringifyEntityRef(ent!)); + + res.json(ratings.filter(r => accessibleEntityRefs.includes(r.userRef))); + }); + + router.post('/responses/:entityRef', async (req, res) => { + const user = await identity.getIdentity({ request: req }); + const { response, comments, consent } = req.body; + + if (!user) { + res.status(400).end(); + return; + } + + await dbHandler.recordResponse({ + entityRef: req.params.entityRef, + response, + comments, + consent, + userRef: user.identity.userEntityRef, + }); + + res.status(201).end(); + }); + + router.get('/responses/:entityRef', async (req, res) => { + const responses = await dbHandler.getResponses(req.params.entityRef); + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + // Filter responses via user refs to only expose entity refs accessible by current user + const accessibleEntityRefs = ( + await catalogClient.getEntitiesByRefs( + { + entityRefs: responses.map(r => r.userRef), + fields: ['kind', 'metadata.namespace', 'metadata.name'], + }, + { token }, + ) + ).items + .filter(Boolean) + .map(ent => stringifyEntityRef(ent!)); + + res.json(responses.filter(r => accessibleEntityRefs.includes(r.userRef))); + }); + + router.use(errorHandler()); + return router; +} diff --git a/plugins/entity-feedback-backend/src/service/standaloneServer.ts b/plugins/entity-feedback-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..9395c72141 --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/standaloneServer.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createServiceBuilder, + DatabaseManager, + loadBackendConfig, + SingleHostDiscovery, + useHotMemoize, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise<Server> { + const logger = options.logger.child({ service: 'entity-feedback-backend' }); + const config = await loadBackendConfig({ logger, argv: process.argv }); + const discovery = SingleHostDiscovery.fromConfig(config); + + const database = useHotMemoize(module, () => { + const manager = DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { client: 'better-sqlite3', connection: ':memory:' }, + }, + }), + ); + return manager.forPlugin('entity-feedback'); + }); + + const identity = DefaultIdentityClient.create({ + discovery, + issuer: await discovery.getExternalBaseUrl('auth'), + }); + + logger.debug('Starting application server...'); + const router = await createRouter({ + database, + discovery, + identity, + logger, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/entity-feedback', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/entity-feedback-backend/src/setupTests.ts b/plugins/entity-feedback-backend/src/setupTests.ts new file mode 100644 index 0000000000..aa70772592 --- /dev/null +++ b/plugins/entity-feedback-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/entity-feedback-common/.eslintrc.js b/plugins/entity-feedback-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/entity-feedback-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/entity-feedback-common/CHANGELOG.md b/plugins/entity-feedback-common/CHANGELOG.md new file mode 100644 index 0000000000..e646a4322d --- /dev/null +++ b/plugins/entity-feedback-common/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/plugin-entity-feedback-common + +## 0.1.0 + +### Minor Changes + +- a3c86a7ed2: Implement entity feedback plugin, check out the `README.md` for more details! diff --git a/plugins/entity-feedback-common/README.md b/plugins/entity-feedback-common/README.md new file mode 100644 index 0000000000..430d14aeab --- /dev/null +++ b/plugins/entity-feedback-common/README.md @@ -0,0 +1,3 @@ +# Entity Feedback Common + +Common types for the entity-feedback plugin. diff --git a/plugins/entity-feedback-common/api-report.md b/plugins/entity-feedback-common/api-report.md new file mode 100644 index 0000000000..118e6c78f2 --- /dev/null +++ b/plugins/entity-feedback-common/api-report.md @@ -0,0 +1,41 @@ +## API Report File for "@backstage/plugin-entity-feedback-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public (undocumented) +export interface EntityRatingsData { + // (undocumented) + entityRef: string; + // (undocumented) + entityTitle?: string; + // (undocumented) + ratings: { + [ratingValue: string]: number; + }; +} + +// @public (undocumented) +export interface FeedbackResponse { + // (undocumented) + comments?: string; + // (undocumented) + consent?: boolean; + // (undocumented) + entityRef: string; + // (undocumented) + response?: string; + // (undocumented) + userRef: string; +} + +// @public (undocumented) +export interface Rating { + // (undocumented) + entityRef: string; + // (undocumented) + rating: string; + // (undocumented) + userRef: string; +} +``` diff --git a/plugins/entity-feedback-common/package.json b/plugins/entity-feedback-common/package.json new file mode 100644 index 0000000000..45a5a2b131 --- /dev/null +++ b/plugins/entity-feedback-common/package.json @@ -0,0 +1,31 @@ +{ + "name": "@backstage/plugin-entity-feedback-common", + "description": "Common functionalities for the entity-feedback plugin", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/entity-feedback-common/src/index.ts b/plugins/entity-feedback-common/src/index.ts new file mode 100644 index 0000000000..eea289a899 --- /dev/null +++ b/plugins/entity-feedback-common/src/index.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common functionalities for the entity-feedback plugin. + * + * @packageDocumentation + */ + +/** + * @public + */ +export interface Rating { + entityRef: string; + rating: string; + userRef: string; +} + +/** + * @public + */ +export interface FeedbackResponse { + entityRef: string; + response?: string; + comments?: string; + consent?: boolean; + userRef: string; +} + +/** + * @public + */ +export interface EntityRatingsData { + entityRef: string; + entityTitle?: string; + ratings: { + [ratingValue: string]: number; + }; +} diff --git a/plugins/entity-feedback-common/src/setupTests.ts b/plugins/entity-feedback-common/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/plugins/entity-feedback-common/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/plugins/entity-feedback/.eslintrc.js b/plugins/entity-feedback/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/entity-feedback/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/entity-feedback/CHANGELOG.md b/plugins/entity-feedback/CHANGELOG.md new file mode 100644 index 0000000000..4fc3561a07 --- /dev/null +++ b/plugins/entity-feedback/CHANGELOG.md @@ -0,0 +1,31 @@ +# @backstage/plugin-entity-feedback + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-entity-feedback-common@0.1.0 + +## 0.1.0 + +### Minor Changes + +- a3c86a7ed2: Implement entity feedback plugin, check out the `README.md` for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/plugin-entity-feedback-common@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 diff --git a/plugins/entity-feedback/README.md b/plugins/entity-feedback/README.md new file mode 100644 index 0000000000..fdfa4761d6 --- /dev/null +++ b/plugins/entity-feedback/README.md @@ -0,0 +1,136 @@ +# Entity Feedback Plugin + +Welcome to the entity-feedback plugin! + +This plugin allows you give and view feedback on entities available in the Backstage catalog. + +## Features + +### Rate entities + +#### Like/Dislike rating + +![Like dislike rating example](./docs/like-dislike-rating.png) + +#### Starred rating + +![Starred rating example](./docs/starred-rating.png) + +### Request additional feedback when poorly rated + +![Response dialog example](./docs/feedback-response-dialog.png) + +### View entity feedback responses + +![Feedback responses example](./docs/feedback-response-table.png) + +### View aggregated ratings on owned entities + +#### Total likes/dislikes + +![Like dislike table example](./docs/like-dislike-table.png) + +#### Star breakdowns + +![Starred rating table example](./docs/starred-rating-table.png) + +## Setup + +The following sections will help you get the Entity Feedback plugin setup and running + +### Backend + +You need to setup the [Entity Feedback backend plugin](https://github.com/backstage/backstage/tree/master/plugins/entity-feedback-backend) before you move forward with any of these steps if you haven't already + +### Installation + +Install this plugin: + +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-entity-feedback +``` + +### Entity Pages + +Add rating and feedback components to your `EntityPage.tsx` to hook up UI so that users +can rate your entities and for owners to view feedback/responses. + +To allow users to apply "Like" and "Dislike" ratings add the following to each kind/type of +entity in your `EntityPage.tsx` you want to be rated (if you prefer to use star ratings, replace +`EntityLikeDislikeRatingsCard` with `EntityStarredRatingsCard` and `LikeDislikeButtons` with +`StarredRatingButtons`): + +```diff +import { + ... ++ InfoCard, + ... +} from '@backstage/core-components'; ++import { ++ EntityFeedbackResponseContent, ++ EntityLikeDislikeRatingsCard, ++ LikeDislikeButtons, ++} from '@backstage/plugin-entity-feedback'; + +// Add to each applicable kind/type of entity as desired +const overviewContent = ( + <Grid container spacing={3} alignItems="stretch"> + ... ++ <Grid item md={2}> ++ <InfoCard title="Rate this entity"> ++ <LikeDislikeButtons /> ++ </InfoCard> ++ </Grid> + ... + </Grid> +); + +... + +// Add to each applicable kind/type of entity as desired +const serviceEntityPage = ( + <EntityLayoutWrapper> + ... ++ <EntityLayout.Route path="/feedback" title="Feedback"> ++ <EntityFeedbackResponseContent /> ++ </EntityLayout.Route> + ... + </EntityLayoutWrapper> +); + +... + +// Add ratings card component to user/group entities to view ratings of owned entities +const userPage = ( + <EntityLayoutWrapper> + <EntityLayout.Route path="/" title="Overview"> + <Grid container spacing={3}> + ... ++ <Grid item xs={12}> ++ <EntityLikeDislikeRatingsCard /> ++ </Grid> + ... + </Grid> + </Grid> + </EntityLayout.Route> + </EntityLayoutWrapper> +); + +const groupPage = ( + <EntityLayoutWrapper> + <EntityLayout.Route path="/" title="Overview"> + <Grid container spacing={3}> + ... ++ <Grid item xs={12}> ++ <EntityLikeDislikeRatingsCard /> ++ </Grid> + ... + </Grid> + </Grid> + </EntityLayout.Route> + </EntityLayoutWrapper> +); +``` + +Note: For a full example of this you can look at [this EntityPage](../../packages/app/src/components/catalog/EntityPage.tsx) diff --git a/plugins/entity-feedback/api-report.md b/plugins/entity-feedback/api-report.md new file mode 100644 index 0000000000..06b0166b0d --- /dev/null +++ b/plugins/entity-feedback/api-report.md @@ -0,0 +1,184 @@ +## API Report File for "@backstage/plugin-entity-feedback" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="react" /> + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { EntityRatingsData } from '@backstage/plugin-entity-feedback-common'; +import { FeedbackResponse } from '@backstage/plugin-entity-feedback-common'; +import { FetchApi } from '@backstage/core-plugin-api'; +import { Rating } from '@backstage/plugin-entity-feedback-common'; +import { ReactNode } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export interface EntityFeedbackApi { + // (undocumented) + getAllRatings(): Promise<EntityRatingsData[]>; + // (undocumented) + getOwnedRatings(ownerRef: string): Promise<EntityRatingsData[]>; + // (undocumented) + getRatings(entityRef: string): Promise<Omit<Rating, 'entityRef'>[]>; + // (undocumented) + getResponses( + entityRef: string, + ): Promise<Omit<FeedbackResponse, 'entityRef'>[]>; + // (undocumented) + recordRating(entityRef: string, rating: string): Promise<void>; + // (undocumented) + recordResponse( + entityRef: string, + response: Omit<FeedbackResponse, 'entityRef' | 'userRef'>, + ): Promise<void>; +} + +// @public (undocumented) +export const entityFeedbackApiRef: ApiRef<EntityFeedbackApi>; + +// @public (undocumented) +export class EntityFeedbackClient implements EntityFeedbackApi { + constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }); + // (undocumented) + getAllRatings(): Promise<EntityRatingsData[]>; + // (undocumented) + getOwnedRatings(ownerRef: string): Promise<EntityRatingsData[]>; + // (undocumented) + getRatings(entityRef: string): Promise<Omit<Rating, 'entityRef'>[]>; + // (undocumented) + getResponses( + entityRef: string, + ): Promise<Omit<FeedbackResponse, 'entityRef'>[]>; + // (undocumented) + recordRating(entityRef: string, rating: string): Promise<void>; + // (undocumented) + recordResponse( + entityRef: string, + response: Omit<FeedbackResponse, 'entityRef' | 'userRef'>, + ): Promise<void>; +} + +// @public (undocumented) +export const entityFeedbackPlugin: BackstagePlugin< + { + root: RouteRef<undefined>; + }, + {}, + {} +>; + +// @public (undocumented) +export interface EntityFeedbackResponse { + // (undocumented) + id: string; + // (undocumented) + label: string; +} + +// @public (undocumented) +export const EntityFeedbackResponseContent: () => JSX.Element; + +// @public (undocumented) +export const EntityLikeDislikeRatingsCard: () => JSX.Element; + +// @public (undocumented) +export const EntityStarredRatingsCard: () => JSX.Element; + +// @public (undocumented) +export const FeedbackResponseDialog: ( + props: FeedbackResponseDialogProps, +) => JSX.Element; + +// @public (undocumented) +export interface FeedbackResponseDialogProps { + // (undocumented) + entity: Entity; + // (undocumented) + feedbackDialogResponses?: EntityFeedbackResponse[]; + // (undocumented) + feedbackDialogTitle?: ReactNode; + // (undocumented) + onClose: () => void; + // (undocumented) + open: boolean; +} + +// @public (undocumented) +export const FeedbackResponseTable: ( + props: FeedbackResponseTableProps, +) => JSX.Element; + +// @public (undocumented) +export interface FeedbackResponseTableProps { + // (undocumented) + entityRef: string; + // (undocumented) + title?: string; +} + +// @public (undocumented) +export const LikeDislikeButtons: ( + props: LikeDislikeButtonsProps, +) => JSX.Element; + +// @public (undocumented) +export interface LikeDislikeButtonsProps { + // (undocumented) + feedbackDialogResponses?: EntityFeedbackResponse[]; + // (undocumented) + feedbackDialogTitle?: ReactNode; + // (undocumented) + requestResponse?: boolean; +} + +// @public (undocumented) +export const LikeDislikeRatingsTable: ( + props: LikeDislikeRatingsTableProps, +) => JSX.Element; + +// @public (undocumented) +export interface LikeDislikeRatingsTableProps { + // (undocumented) + allEntities?: boolean; + // (undocumented) + ownerRef?: string; + // (undocumented) + title?: string; +} + +// @public (undocumented) +export const StarredRatingButtons: ( + props: StarredRatingButtonsProps, +) => JSX.Element; + +// @public (undocumented) +export interface StarredRatingButtonsProps { + // (undocumented) + feedbackDialogResponses?: EntityFeedbackResponse[]; + // (undocumented) + feedbackDialogTitle?: ReactNode; + // (undocumented) + requestResponse?: boolean; + // (undocumented) + requestResponseThreshold?: number; +} + +// @public (undocumented) +export const StarredRatingsTable: ( + props: StarredRatingsTableProps, +) => JSX.Element; + +// @public (undocumented) +export interface StarredRatingsTableProps { + // (undocumented) + allEntities?: boolean; + // (undocumented) + ownerRef?: string; + // (undocumented) + title?: string; +} +``` diff --git a/plugins/entity-feedback/docs/feedback-response-dialog.png b/plugins/entity-feedback/docs/feedback-response-dialog.png new file mode 100644 index 0000000000..74270c805a Binary files /dev/null and b/plugins/entity-feedback/docs/feedback-response-dialog.png differ diff --git a/plugins/entity-feedback/docs/feedback-response-table.png b/plugins/entity-feedback/docs/feedback-response-table.png new file mode 100644 index 0000000000..3795ac809d Binary files /dev/null and b/plugins/entity-feedback/docs/feedback-response-table.png differ diff --git a/plugins/entity-feedback/docs/like-dislike-rating.png b/plugins/entity-feedback/docs/like-dislike-rating.png new file mode 100644 index 0000000000..0267218243 Binary files /dev/null and b/plugins/entity-feedback/docs/like-dislike-rating.png differ diff --git a/plugins/entity-feedback/docs/like-dislike-table.png b/plugins/entity-feedback/docs/like-dislike-table.png new file mode 100644 index 0000000000..df38883d25 Binary files /dev/null and b/plugins/entity-feedback/docs/like-dislike-table.png differ diff --git a/plugins/entity-feedback/docs/starred-rating-table.png b/plugins/entity-feedback/docs/starred-rating-table.png new file mode 100644 index 0000000000..e678c5b68a Binary files /dev/null and b/plugins/entity-feedback/docs/starred-rating-table.png differ diff --git a/plugins/entity-feedback/docs/starred-rating.png b/plugins/entity-feedback/docs/starred-rating.png new file mode 100644 index 0000000000..6198f7ae66 Binary files /dev/null and b/plugins/entity-feedback/docs/starred-rating.png differ diff --git a/plugins/entity-feedback/package.json b/plugins/entity-feedback/package.json new file mode 100644 index 0000000000..bcf3895d7b --- /dev/null +++ b/plugins/entity-feedback/package.json @@ -0,0 +1,63 @@ +{ + "name": "@backstage/plugin-entity-feedback", + "version": "0.1.1-next.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/entity-feedback" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-entity-feedback-common": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/entity-feedback/src/api/EntityFeedbackApi.ts b/plugins/entity-feedback/src/api/EntityFeedbackApi.ts new file mode 100644 index 0000000000..d2f1a46f55 --- /dev/null +++ b/plugins/entity-feedback/src/api/EntityFeedbackApi.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core-plugin-api'; +import { + EntityRatingsData, + FeedbackResponse, + Rating, +} from '@backstage/plugin-entity-feedback-common'; + +/** + * @public + */ +export const entityFeedbackApiRef = createApiRef<EntityFeedbackApi>({ + id: 'plugin.entity-feedback.service', +}); + +/** + * @public + */ +export interface EntityFeedbackApi { + getAllRatings(): Promise<EntityRatingsData[]>; + + getOwnedRatings(ownerRef: string): Promise<EntityRatingsData[]>; + + recordRating(entityRef: string, rating: string): Promise<void>; + + getRatings(entityRef: string): Promise<Omit<Rating, 'entityRef'>[]>; + + recordResponse( + entityRef: string, + response: Omit<FeedbackResponse, 'entityRef' | 'userRef'>, + ): Promise<void>; + + getResponses( + entityRef: string, + ): Promise<Omit<FeedbackResponse, 'entityRef'>[]>; +} diff --git a/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts b/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts new file mode 100644 index 0000000000..694fd66b06 --- /dev/null +++ b/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +import { EntityFeedbackClient } from './EntityFeedbackClient'; + +const server = setupServer(); + +describe('EntityFeedbackClient', () => { + setupRequestMockHandlers(server); + const mockBaseUrl = 'http://backstage/api/entity-feedback'; + const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; + const fetchApi = new MockFetchApi(); + + let client: EntityFeedbackClient; + beforeEach(() => { + client = new EntityFeedbackClient({ discoveryApi, fetchApi }); + }); + + it('getAllRatings', async () => { + const ratings = [ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo', + ratings: { LIKE: 10 }, + }, + { + entityRef: 'component:default/bar', + entityTitle: 'Bar', + ratings: { DISLIKE: 10 }, + }, + ]; + + server.use( + rest.get(`${mockBaseUrl}/ratings`, (_, res, ctx) => + res(ctx.json(ratings)), + ), + ); + + const response = await client.getAllRatings(); + expect(response).toEqual(ratings); + }); + + it('getOwnedRatings', async () => { + const ratings = [ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo', + ratings: { LIKE: 10 }, + }, + { + entityRef: 'component:default/bar', + entityTitle: 'Bar', + ratings: { DISLIKE: 10 }, + }, + ]; + + server.use( + rest.get( + `${mockBaseUrl}/ratings?ownerRef=${encodeURIComponent( + 'group:default/team', + )}`, + (_, res, ctx) => res(ctx.json(ratings)), + ), + ); + + const response = await client.getOwnedRatings('group:default/team'); + expect(response).toEqual(ratings); + }); + + it('recordRating', async () => { + expect.assertions(1); + + server.use( + rest.post( + `${mockBaseUrl}/ratings/${encodeURIComponent( + 'component:default/service', + )}`, + (req, res) => { + expect(req.body).toEqual({ rating: 'LIKE' }); + return res(); + }, + ), + ); + + await client.recordRating('component:default/service', 'LIKE'); + }); + + it('getRatings', async () => { + const ratings = [ + { userRef: 'user:default/foo', rating: 'LIKE' }, + { userRef: 'user:default/bar', rating: 'LIKE' }, + ]; + + server.use( + rest.get( + `${mockBaseUrl}/ratings/${encodeURIComponent( + 'component:default/service', + )}`, + (_, res, ctx) => res(ctx.json(ratings)), + ), + ); + + const response = await client.getRatings('component:default/service'); + expect(response).toEqual(ratings); + }); + + it('recordResponse', async () => { + expect.assertions(1); + const response = { + response: 'blah', + comments: 'feedback', + consent: false, + }; + + server.use( + rest.post( + `${mockBaseUrl}/responses/${encodeURIComponent( + 'component:default/service', + )}`, + (req, res) => { + expect(req.body).toEqual(response); + return res(); + }, + ), + ); + + await client.recordResponse('component:default/service', response); + }); + + it('getResponses', async () => { + const responses = [ + { + userRef: 'user:default/foo', + response: 'asdf', + comments: 'here is new feedback', + consent: false, + }, + { + userRef: 'user:default/bar', + response: 'noop', + comments: 'here is different feedback', + consent: true, + }, + ]; + + server.use( + rest.get( + `${mockBaseUrl}/responses/${encodeURIComponent( + 'component:default/service', + )}`, + (_, res, ctx) => res(ctx.json(responses)), + ), + ); + + const response = await client.getResponses('component:default/service'); + expect(response).toEqual(responses); + }); +}); diff --git a/plugins/entity-feedback/src/api/EntityFeedbackClient.ts b/plugins/entity-feedback/src/api/EntityFeedbackClient.ts new file mode 100644 index 0000000000..34856473c8 --- /dev/null +++ b/plugins/entity-feedback/src/api/EntityFeedbackClient.ts @@ -0,0 +1,136 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { + EntityRatingsData, + FeedbackResponse, + Rating, +} from '@backstage/plugin-entity-feedback-common'; + +import { EntityFeedbackApi } from './EntityFeedbackApi'; + +/** + * @public + */ +export class EntityFeedbackClient implements EntityFeedbackApi { + private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; + + constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) { + this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi; + } + + async getAllRatings(): Promise<EntityRatingsData[]> { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch(`${baseUrl}/ratings`, { + method: 'GET', + }); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + + return resp.json(); + } + + async getOwnedRatings(ownerRef: string): Promise<EntityRatingsData[]> { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/ratings?ownerRef=${encodeURIComponent(ownerRef)}`, + { + method: 'GET', + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + + return resp.json(); + } + + async recordRating(entityRef: string, rating: string) { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/ratings/${encodeURIComponent(entityRef)}`, + { + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + body: JSON.stringify({ rating }), + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + } + + async getRatings(entityRef: string): Promise<Omit<Rating, 'entityRef'>[]> { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/ratings/${encodeURIComponent(entityRef)}`, + { + method: 'GET', + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + + return resp.json(); + } + + async recordResponse( + entityRef: string, + response: Omit<FeedbackResponse, 'entityRef' | 'userRef'>, + ) { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/responses/${encodeURIComponent(entityRef)}`, + { + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + body: JSON.stringify(response), + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + } + + async getResponses( + entityRef: string, + ): Promise<Omit<FeedbackResponse, 'entityRef'>[]> { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/responses/${encodeURIComponent(entityRef)}`, + { + method: 'GET', + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + + return resp.json(); + } +} diff --git a/plugins/entity-feedback/src/api/index.ts b/plugins/entity-feedback/src/api/index.ts new file mode 100644 index 0000000000..32c8624425 --- /dev/null +++ b/plugins/entity-feedback/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './EntityFeedbackClient'; +export * from './EntityFeedbackApi'; diff --git a/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.test.tsx b/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.test.tsx new file mode 100644 index 0000000000..b639b2e727 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.test.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackRatingsTable } from './FeedbackRatingsTable'; + +describe('FeedbackRatingsTable', () => { + const sampleRatings = [ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo Component', + ratings: { 'Rating 1': 3, 'Rating 2': 1 }, + }, + { + entityRef: 'system:default/bar', + entityTitle: 'Bar Component', + ratings: { 'Rating 1': 5 }, + }, + { + entityRef: 'domain:default/hello-world', + entityTitle: 'Hello World', + ratings: { 'Rating 3': 5 }, + }, + ]; + + const feedbackApi: Partial<EntityFeedbackApi> = { + getAllRatings: jest.fn().mockImplementation(async () => sampleRatings), + getOwnedRatings: jest.fn().mockImplementation(async () => sampleRatings), + }; + + const sampleRatingValues = ['Rating 1', 'Rating 2']; + + const render = async (props: any = {}) => + renderInTestApp( + <TestApiProvider apis={[[entityFeedbackApiRef, feedbackApi]]}> + <FeedbackRatingsTable {...props} ratingValues={sampleRatingValues} /> + </TestApiProvider>, + { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }, + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders all ratings correctly', async () => { + const rendered = await render({ allEntities: true }); + + expect(feedbackApi.getAllRatings).toHaveBeenCalled(); + expect(feedbackApi.getOwnedRatings).not.toHaveBeenCalled(); + + expect(rendered.getByText('Entity Ratings')).toBeInTheDocument(); + + // Columns + expect(rendered.getByText('Entity')).toBeInTheDocument(); + expect(rendered.getByText('Rating 1')).toBeInTheDocument(); + expect(rendered.getByText('Rating 2')).toBeInTheDocument(); + + // Rows + expect(rendered.getByText('Foo Component')).toBeInTheDocument(); + expect(rendered.getByText('component')).toBeInTheDocument(); + expect(rendered.getByText('3')).toBeInTheDocument(); + expect(rendered.getByText('1')).toBeInTheDocument(); + expect(rendered.getByText('Bar Component')).toBeInTheDocument(); + expect(rendered.getByText('system')).toBeInTheDocument(); + expect(rendered.getByText('5')).toBeInTheDocument(); + + expect(rendered.queryByText('Hello World')).toBeNull(); + }); + + it('renders owned entity ratings correctly', async () => { + const rendered = await render({ + ownerRef: 'group:default/test-team', + title: 'Custom Title', + }); + + expect(feedbackApi.getAllRatings).not.toHaveBeenCalled(); + expect(feedbackApi.getOwnedRatings).toHaveBeenCalledWith( + 'group:default/test-team', + ); + + expect(rendered.getByText('Custom Title')).toBeInTheDocument(); + + // Columns + expect(rendered.getByText('Entity')).toBeInTheDocument(); + expect(rendered.getByText('Rating 1')).toBeInTheDocument(); + expect(rendered.getByText('Rating 2')).toBeInTheDocument(); + + // Rows + expect(rendered.getByText('Foo Component')).toBeInTheDocument(); + expect(rendered.getByText('component')).toBeInTheDocument(); + expect(rendered.getByText('3')).toBeInTheDocument(); + expect(rendered.getByText('1')).toBeInTheDocument(); + expect(rendered.getByText('Bar Component')).toBeInTheDocument(); + expect(rendered.getByText('system')).toBeInTheDocument(); + expect(rendered.getByText('5')).toBeInTheDocument(); + + expect(rendered.queryByText('Hello World')).toBeNull(); + }); +}); diff --git a/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.tsx b/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.tsx new file mode 100644 index 0000000000..48b2018729 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { parseEntityRef } from '@backstage/catalog-model'; +import { ErrorPanel, SubvalueCell, Table } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { EntityRefLink } from '@backstage/plugin-catalog-react'; +import { EntityRatingsData } from '@backstage/plugin-entity-feedback-common'; +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; + +import { entityFeedbackApiRef } from '../../api'; + +interface FeedbackRatingsTableProps { + allEntities?: boolean; + ownerRef?: string; + ratingValues: string[]; + title?: string; +} + +export const FeedbackRatingsTable = (props: FeedbackRatingsTableProps) => { + const { + allEntities, + ownerRef, + ratingValues, + title = 'Entity Ratings', + } = props; + const feedbackApi = useApi(entityFeedbackApiRef); + + const { + error, + loading, + value: ratings, + } = useAsync(async () => { + if (allEntities) { + return feedbackApi.getAllRatings(); + } + + if (!ownerRef) { + return []; + } + + return feedbackApi.getOwnedRatings(ownerRef); + }, [allEntities, feedbackApi, ownerRef]); + + const columns = [ + { title: 'Title', field: 'entityTitle', hidden: true, searchable: true }, + { + title: 'Entity', + field: 'entityRef', + highlight: true, + customSort: (a: EntityRatingsData, b: EntityRatingsData) => { + const titleA = a.entityTitle ?? parseEntityRef(a.entityRef).name; + const titleB = b.entityTitle ?? parseEntityRef(b.entityRef).name; + return titleA.localeCompare(titleB); + }, + render: (rating: EntityRatingsData) => { + const compoundRef = parseEntityRef(rating.entityRef); + return ( + <SubvalueCell + value={ + <EntityRefLink + entityRef={rating.entityRef} + defaultKind={compoundRef.kind} + title={rating.entityTitle} + /> + } + subvalue={compoundRef.kind} + /> + ); + }, + }, + ...ratingValues.map(ratingVal => ({ + title: ratingVal, + field: `ratings.${ratingVal}`, + })), + ]; + + // Exclude entities that don't have applicable ratings + const ratingsRows = ratings?.filter(r => + Object.keys(r.ratings).some(v => ratingValues.includes(v)), + ); + + if (error) { + return ( + <ErrorPanel + defaultExpanded + title="Failed to load feedback ratings" + error={error} + /> + ); + } + + return ( + <Table<EntityRatingsData> + columns={columns} + data={ratingsRows ?? []} + isLoading={loading} + options={{ + emptyRowsWhenPaging: false, + loadingType: 'linear', + pageSize: 20, + pageSizeOptions: [20, 50, 100], + paging: true, + showEmptyDataSourceMessage: !loading, + }} + title={title} + /> + ); +}; diff --git a/plugins/entity-feedback/src/components/FeedbackRatingsTable/index.ts b/plugins/entity-feedback/src/components/FeedbackRatingsTable/index.ts new file mode 100644 index 0000000000..565063b3fa --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackRatingsTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './FeedbackRatingsTable'; diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx new file mode 100644 index 0000000000..631738d6f9 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { ErrorApi, errorApiRef } from '@backstage/core-plugin-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { getByRole, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackResponseDialog } from './FeedbackResponseDialog'; + +describe('FeedbackResponseDialog', () => { + const testEntity: Partial<Entity> = { + kind: 'component', + metadata: { name: 'test', namespace: 'default' }, + }; + const errorApi: Partial<ErrorApi> = { post: jest.fn() }; + const feedbackApi: Partial<EntityFeedbackApi> = { + recordResponse: jest.fn().mockImplementation(() => Promise.resolve()), + }; + + const render = async (props: any = {}) => + renderInTestApp( + <TestApiProvider + apis={[ + [entityFeedbackApiRef, feedbackApi], + [errorApiRef, errorApi], + ]} + > + <FeedbackResponseDialog + {...props} + entity={testEntity} + open + onClose={jest.fn()} + /> + </TestApiProvider>, + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('allows customization of the dialog title', async () => { + const rendered = await render(); + expect( + rendered.getByText('Please provide feedback on what can be improved'), + ).toBeInTheDocument(); + + const customRendered = await render({ feedbackDialogTitle: 'Test Title' }); + expect(customRendered.getByText('Test Title')).toBeInTheDocument(); + }); + + it('allows customization of the reponse options', async () => { + const rendered = await render(); + expect(rendered.getByText('Incorrect info')).toBeInTheDocument(); + expect(rendered.getByText('Missing info')).toBeInTheDocument(); + expect( + rendered.getByText('Other (please specify below)'), + ).toBeInTheDocument(); + + const customResponses = [ + { id: 'foo', label: 'Foo option' }, + { id: 'bar', label: 'Bar option' }, + ]; + const customRendered = await render({ + feedbackDialogResponses: customResponses, + }); + expect(customRendered.getByText('Foo option')).toBeInTheDocument(); + expect(customRendered.getByText('Bar option')).toBeInTheDocument(); + }); + + it('handles saving user responses', async () => { + const rendered = await render(); + + await userEvent.click( + rendered.getByRole('checkbox', { name: 'Incorrect info' }), + ); + await userEvent.click( + rendered.getByRole('checkbox', { name: 'Other (please specify below)' }), + ); + await userEvent.type( + getByRole( + rendered.getByTestId('feedback-response-dialog-comments-input'), + 'textbox', + ), + 'test comments', + ); + await userEvent.click( + rendered.getByTestId('feedback-response-dialog-submit-button'), + ); + + await waitFor(() => { + expect(feedbackApi.recordResponse).toHaveBeenCalledWith( + 'component:default/test', + { + comments: 'test comments', + consent: true, + response: 'incorrect,other', + }, + ); + }); + }); +}); diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx new file mode 100644 index 0000000000..c3f935e432 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx @@ -0,0 +1,176 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { Progress } from '@backstage/core-components'; +import { ErrorApiError, errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControl, + FormControlLabel, + FormGroup, + FormLabel, + Grid, + makeStyles, + Switch, + TextField, + Typography, +} from '@material-ui/core'; +import React, { ReactNode, useState } from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +import { entityFeedbackApiRef } from '../../api'; + +/** + * @public + */ +export interface EntityFeedbackResponse { + id: string; + label: string; +} + +const defaultFeedbackResponses: EntityFeedbackResponse[] = [ + { id: 'incorrect', label: 'Incorrect info' }, + { id: 'missing', label: 'Missing info' }, + { id: 'other', label: 'Other (please specify below)' }, +]; + +/** + * @public + */ +export interface FeedbackResponseDialogProps { + entity: Entity; + feedbackDialogResponses?: EntityFeedbackResponse[]; + feedbackDialogTitle?: ReactNode; + open: boolean; + onClose: () => void; +} + +const useStyles = makeStyles({ + contactConsent: { + marginTop: '5px', + }, +}); + +export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { + const { + entity, + feedbackDialogResponses = defaultFeedbackResponses, + feedbackDialogTitle = 'Please provide feedback on what can be improved', + open, + onClose, + } = props; + const classes = useStyles(); + const errorApi = useApi(errorApiRef); + const feedbackApi = useApi(entityFeedbackApiRef); + const [responseSelections, setResponseSelections] = useState( + Object.fromEntries(feedbackDialogResponses.map(r => [r.id, false])), + ); + const [comments, setComments] = useState(''); + const [consent, setConsent] = useState(true); + + const [{ loading: saving }, saveResponse] = useAsyncFn(async () => { + try { + await feedbackApi.recordResponse(stringifyEntityRef(entity), { + comments, + consent, + response: Object.keys(responseSelections) + .filter(id => responseSelections[id]) + .join(','), + }); + onClose(); + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, [comments, consent, entity, feedbackApi, onClose, responseSelections]); + + return ( + <Dialog open={open} onClose={() => !saving && onClose()}> + {saving && <Progress />} + <DialogTitle>{feedbackDialogTitle}</DialogTitle> + <DialogContent> + <FormControl component="fieldset"> + <FormLabel component="legend">Choose all that applies</FormLabel> + <FormGroup> + {feedbackDialogResponses.map(response => ( + <FormControlLabel + key={response.id} + control={ + <Checkbox + checked={responseSelections[response.id]} + disabled={saving} + name={response.id} + onChange={e => + setResponseSelections({ + ...responseSelections, + [e.target.name]: e.target.checked, + }) + } + /> + } + label={response.label} + /> + ))} + </FormGroup> + </FormControl> + <FormControl fullWidth> + <TextField + data-testid="feedback-response-dialog-comments-input" + disabled={saving} + label="Additional comments" + multiline + minRows={2} + onChange={e => setComments(e.target.value)} + variant="outlined" + value={comments} + /> + </FormControl> + <Typography className={classes.contactConsent}> + Can we reach out to you for more info? + <Grid component="label" container alignItems="center" spacing={1}> + <Grid item>No</Grid> + <Grid item> + <Switch + checked={consent} + disabled={saving} + onChange={e => setConsent(e.target.checked)} + /> + </Grid> + <Grid item>Yes</Grid> + </Grid> + </Typography> + </DialogContent> + <DialogActions> + <Button color="primary" disabled={saving} onClick={onClose}> + Close + </Button> + <Button + color="primary" + data-testid="feedback-response-dialog-submit-button" + disabled={saving} + onClick={saveResponse} + > + Submit + </Button> + </DialogActions> + </Dialog> + ); +}; diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/index.ts b/plugins/entity-feedback/src/components/FeedbackResponseDialog/index.ts new file mode 100644 index 0000000000..8ce88f4b87 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './FeedbackResponseDialog'; diff --git a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.test.tsx b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.test.tsx new file mode 100644 index 0000000000..c5d3d51884 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackResponseTable } from './FeedbackResponseTable'; + +describe('FeedbackResponseTable', () => { + const sampleResponses = [ + { + userRef: 'user:default/foo', + consent: true, + response: 'resp1,resp2', + comments: 'test comment 1', + }, + { + userRef: 'user:default/bar', + consent: false, + response: 'resp3,resp4', + comments: 'test comment 2', + }, + ]; + + const feedbackApi: Partial<EntityFeedbackApi> = { + getResponses: jest.fn().mockImplementation(async () => sampleResponses), + }; + + const render = async (props: any = {}) => + renderInTestApp( + <TestApiProvider apis={[[entityFeedbackApiRef, feedbackApi]]}> + <FeedbackResponseTable {...props} entityRef="component:default/test" /> + </TestApiProvider>, + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders all responses correctly', async () => { + const rendered = await render(); + + expect(feedbackApi.getResponses).toHaveBeenCalledWith( + 'component:default/test', + ); + + expect(rendered.getByText('Entity Responses')).toBeInTheDocument(); + + expect(rendered.getByText('foo')).toBeInTheDocument(); + expect(rendered.getByText('resp1')).toBeInTheDocument(); + expect(rendered.getByText('resp2')).toBeInTheDocument(); + expect(rendered.getByText('test comment 1')).toBeInTheDocument(); + expect(rendered.getByText('bar')).toBeInTheDocument(); + expect(rendered.getByText('resp3')).toBeInTheDocument(); + expect(rendered.getByText('resp4')).toBeInTheDocument(); + expect(rendered.getByText('test comment 2')).toBeInTheDocument(); + }); + + it('renders a custom title correctly', async () => { + const rendered = await render({ title: 'Custom Title' }); + expect(rendered.getByText('Custom Title')).toBeInTheDocument(); + }); +}); diff --git a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx new file mode 100644 index 0000000000..ba954c7747 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx @@ -0,0 +1,121 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { parseEntityRef } from '@backstage/catalog-model'; +import { ErrorPanel, Table } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; +import { FeedbackResponse } from '@backstage/plugin-entity-feedback-common'; +import { BackstageTheme } from '@backstage/theme'; +import { Chip, makeStyles } from '@material-ui/core'; +import CheckIcon from '@material-ui/icons/Check'; +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; + +import { entityFeedbackApiRef } from '../../api'; + +type ResponseRow = Omit<FeedbackResponse, 'entityRef'>; + +const useStyles = makeStyles<BackstageTheme>(theme => ({ + consentCheck: { + color: theme.palette.status.ok, + }, +})); + +/** + * @public + */ +export interface FeedbackResponseTableProps { + entityRef: string; + title?: string; +} + +export const FeedbackResponseTable = (props: FeedbackResponseTableProps) => { + const { entityRef, title = 'Entity Responses' } = props; + const classes = useStyles(); + const feedbackApi = useApi(entityFeedbackApiRef); + + const { + error, + loading, + value: responses, + } = useAsync(async () => { + if (!entityRef) { + return []; + } + + return feedbackApi.getResponses(entityRef); + }, [entityRef, feedbackApi]); + + const columns = [ + { + title: 'User', + field: 'userRef', + width: '15%', + render: (response: ResponseRow) => + humanizeEntityRef(parseEntityRef(response.userRef), { + defaultKind: 'user', + }), + }, + { + title: 'OK to contact?', + field: 'consent', + width: '10%', + render: (response: ResponseRow) => + response.consent ? <CheckIcon className={classes.consentCheck} /> : '', + }, + { + title: 'Responses', + field: 'response', + width: '35%', + render: (response: ResponseRow) => ( + <> + {response.response?.split(',').map(res => ( + <Chip key={res} size="small" label={res} /> + ))} + </> + ), + }, + { title: 'Comments', field: 'comments', width: '40%' }, + ]; + + if (error) { + return ( + <ErrorPanel + defaultExpanded + title="Failed to load feedback responses" + error={error} + /> + ); + } + + return ( + <Table<ResponseRow> + columns={columns} + data={(responses ?? []) as ResponseRow[]} + isLoading={loading} + options={{ + emptyRowsWhenPaging: false, + loadingType: 'linear', + pageSize: 20, + pageSizeOptions: [20, 50, 100], + paging: true, + showEmptyDataSourceMessage: !loading, + }} + title={title} + /> + ); +}; diff --git a/plugins/entity-feedback/src/components/FeedbackResponseTable/index.ts b/plugins/entity-feedback/src/components/FeedbackResponseTable/index.ts new file mode 100644 index 0000000000..73f4632fbf --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './FeedbackResponseTable'; diff --git a/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.test.tsx b/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.test.tsx new file mode 100644 index 0000000000..1edf578d3e --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.test.tsx @@ -0,0 +1,147 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { + ErrorApi, + errorApiRef, + IdentityApi, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { AsyncEntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackRatings, LikeDislikeButtons } from './LikeDislikeButtons'; + +jest.mock('../FeedbackResponseDialog', () => ({ + FeedbackResponseDialog: ({ open }: { open: boolean }) => { + return <>{open && <span>dialog is open</span>}</>; + }, +})); + +describe('LikeDislikeButtons', () => { + const sampleRatings = [ + { + userRef: 'user:default/me', + rating: FeedbackRatings.like, + }, + { + userRef: 'user:default/someone', + rating: FeedbackRatings.dislike, + }, + ]; + + const testEntity = { + kind: 'component', + metadata: { name: 'test', namespace: 'default' }, + } as Entity; + + const errorApi: Partial<ErrorApi> = { post: jest.fn() }; + + const feedbackApi: Partial<EntityFeedbackApi> = { + getRatings: jest.fn().mockImplementation(async () => sampleRatings), + recordRating: jest.fn().mockImplementation(() => Promise.resolve()), + }; + + const identityApi: Partial<IdentityApi> = { + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'user:default/me', + ownershipEntityRefs: [], + }), + }; + + const render = async (props: any = {}) => + renderInTestApp( + <TestApiProvider + apis={[ + [entityFeedbackApiRef, feedbackApi], + [errorApiRef, errorApi], + [identityApiRef, identityApi], + ]} + > + <AsyncEntityProvider loading={false} entity={testEntity}> + <LikeDislikeButtons {...props} /> + </AsyncEntityProvider> + </TestApiProvider>, + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('loads the previous rating if it exists', async () => { + await render(); + expect(feedbackApi.getRatings).toHaveBeenCalledWith( + 'component:default/test', + ); + }); + + it('applies a rating correctly', async () => { + const rendered = await render(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-dislike-button'), + ); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.dislike, + ); + + jest.clearAllMocks(); + + await userEvent.click(rendered.getByTestId('entity-feedback-like-button')); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.like, + ); + }); + + it('removes an existing rating correctly', async () => { + const rendered = await render(); + + await userEvent.click(rendered.getByTestId('entity-feedback-like-button')); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.neutral, + ); + }); + + it('opens a response dialog if dislike is selected', async () => { + const rendered = await render(); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-dislike-button'), + ); + expect(rendered.getByText('dialog is open')).toBeInTheDocument(); + }); + + it('does not open a response dialog on dislike if configured not to', async () => { + const rendered = await render({ requestResponse: false }); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-dislike-button'), + ); + expect(rendered.queryByText('dialog is open')).toBeNull(); + }); +}); diff --git a/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.tsx b/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.tsx new file mode 100644 index 0000000000..2d38e60ac2 --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Progress } from '@backstage/core-components'; +import { + ErrorApiError, + errorApiRef, + identityApiRef, + useApi, +} from '@backstage/core-plugin-api'; +import { useAsyncEntity } from '@backstage/plugin-catalog-react'; +import { IconButton } from '@material-ui/core'; +import ThumbDownIcon from '@material-ui/icons/ThumbDown'; +import ThumbUpIcon from '@material-ui/icons/ThumbUp'; +import ThumbDownOutlinedIcon from '@material-ui/icons/ThumbDownOutlined'; +import ThumbUpOutlinedIcon from '@material-ui/icons/ThumbUpOutlined'; +import React, { ReactNode, useCallback, useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +import { entityFeedbackApiRef } from '../../api'; +import { + EntityFeedbackResponse, + FeedbackResponseDialog, +} from '../FeedbackResponseDialog'; + +export enum FeedbackRatings { + like = 'LIKE', + dislike = 'DISLIKE', + neutral = 'NEUTRAL', +} + +/** + * @public + */ +export interface LikeDislikeButtonsProps { + feedbackDialogResponses?: EntityFeedbackResponse[]; + feedbackDialogTitle?: ReactNode; + requestResponse?: boolean; +} + +export const LikeDislikeButtons = (props: LikeDislikeButtonsProps) => { + const { + feedbackDialogResponses, + feedbackDialogTitle, + requestResponse = true, + } = props; + const errorApi = useApi(errorApiRef); + const feedbackApi = useApi(entityFeedbackApiRef); + const identityApi = useApi(identityApiRef); + const [rating, setRating] = useState<FeedbackRatings>( + FeedbackRatings.neutral, + ); + const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false); + const { entity, loading: loadingEntity } = useAsyncEntity(); + + const { loading: loadingFeedback } = useAsync(async () => { + // Wait until entity is loaded + if (!entity) { + return; + } + + try { + const identity = await identityApi.getBackstageIdentity(); + const prevFeedback = await feedbackApi.getRatings( + stringifyEntityRef(entity), + ); + setRating( + (prevFeedback.find(r => r.userRef === identity.userEntityRef)?.rating ?? + rating) as FeedbackRatings, + ); + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, [entity, feedbackApi, setRating]); + + const [{ loading: savingFeedback }, saveFeedback] = useAsyncFn( + async (feedback: FeedbackRatings) => { + try { + await feedbackApi.recordRating(stringifyEntityRef(entity!), feedback); + setRating(feedback); + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, + [entity, feedbackApi, setRating], + ); + + const applyRating = useCallback( + (feedback: FeedbackRatings) => { + // Clear rating if feedback is same as current + if (feedback === rating) { + saveFeedback(FeedbackRatings.neutral); + return; + } + + saveFeedback(feedback); + if (feedback === FeedbackRatings.dislike && requestResponse) { + setOpenFeedbackDialog(true); + } + }, + [rating, requestResponse, saveFeedback, setOpenFeedbackDialog], + ); + + if (loadingEntity || loadingFeedback || savingFeedback) { + return <Progress />; + } + + return ( + <> + <IconButton + data-testid="entity-feedback-like-button" + onClick={() => applyRating(FeedbackRatings.like)} + > + {rating === FeedbackRatings.like ? ( + <ThumbUpIcon fontSize="small" /> + ) : ( + <ThumbUpOutlinedIcon fontSize="small" /> + )} + </IconButton> + <IconButton + data-testid="entity-feedback-dislike-button" + onClick={() => applyRating(FeedbackRatings.dislike)} + > + {rating === FeedbackRatings.dislike ? ( + <ThumbDownIcon fontSize="small" /> + ) : ( + <ThumbDownOutlinedIcon fontSize="small" /> + )} + </IconButton> + <FeedbackResponseDialog + entity={entity!} + open={openFeedbackDialog} + onClose={() => setOpenFeedbackDialog(false)} + feedbackDialogResponses={feedbackDialogResponses} + feedbackDialogTitle={feedbackDialogTitle} + /> + </> + ); +}; diff --git a/plugins/entity-feedback/src/components/LikeDislikeButtons/index.ts b/plugins/entity-feedback/src/components/LikeDislikeButtons/index.ts new file mode 100644 index 0000000000..58038c32e1 --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeButtons/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './LikeDislikeButtons'; diff --git a/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.test.tsx b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.test.tsx new file mode 100644 index 0000000000..9f04d32ece --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; + +import { FeedbackRatings } from '../LikeDislikeButtons'; +import { LikeDislikeRatingsTable } from './LikeDislikeRatingsTable'; + +jest.mock('../FeedbackRatingsTable', () => ({ + FeedbackRatingsTable: ({ ratingValues }: { ratingValues: string[] }) => { + return ( + <span> + {ratingValues.map(v => ( + <span key={v}>{v}</span> + ))} + </span> + ); + }, +})); + +describe('LikeDislikeRatingsTable', () => { + it('renders like-dislike ratings correctly', async () => { + const rendered = await renderInTestApp(<LikeDislikeRatingsTable />); + expect(rendered.getByText(FeedbackRatings.like)).toBeInTheDocument(); + expect(rendered.queryByText(FeedbackRatings.neutral)).toBeNull(); + expect(rendered.getByText(FeedbackRatings.dislike)).toBeInTheDocument(); + }); +}); diff --git a/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.tsx b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.tsx new file mode 100644 index 0000000000..5466ba2b03 --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import { FeedbackRatingsTable } from '../FeedbackRatingsTable'; +import { FeedbackRatings } from '../LikeDislikeButtons'; + +/** + * @public + */ +export interface LikeDislikeRatingsTableProps { + allEntities?: boolean; + ownerRef?: string; + title?: string; +} + +export const LikeDislikeRatingsTable = ( + props: LikeDislikeRatingsTableProps, +) => { + const { allEntities, ownerRef, title } = props; + + return ( + <FeedbackRatingsTable + allEntities={allEntities} + ownerRef={ownerRef} + ratingValues={Object.values(FeedbackRatings).filter( + r => r !== FeedbackRatings.neutral, + )} + title={title} + /> + ); +}; diff --git a/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/index.ts b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/index.ts new file mode 100644 index 0000000000..d093b8bd2c --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './LikeDislikeRatingsTable'; diff --git a/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.test.tsx b/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.test.tsx new file mode 100644 index 0000000000..82fc843494 --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.test.tsx @@ -0,0 +1,159 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { + ErrorApi, + errorApiRef, + IdentityApi, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { AsyncEntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackRatings, StarredRatingButtons } from './StarredRatingButtons'; + +jest.mock('../FeedbackResponseDialog', () => ({ + FeedbackResponseDialog: ({ open }: { open: boolean }) => { + return <>{open && <span>dialog is open</span>}</>; + }, +})); + +describe('StarredRatingButtons', () => { + const sampleRatings = [ + { + userRef: 'user:default/me', + rating: FeedbackRatings.two, + }, + { + userRef: 'user:default/someone', + rating: FeedbackRatings.five, + }, + ]; + + const testEntity = { + kind: 'component', + metadata: { name: 'test', namespace: 'default' }, + } as Entity; + + const errorApi: Partial<ErrorApi> = { post: jest.fn() }; + + const feedbackApi: Partial<EntityFeedbackApi> = { + getRatings: jest.fn().mockImplementation(async () => sampleRatings), + recordRating: jest.fn().mockImplementation(() => Promise.resolve()), + }; + + const identityApi: Partial<IdentityApi> = { + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'user:default/me', + ownershipEntityRefs: [], + }), + }; + + const render = async (props: any = {}) => + renderInTestApp( + <TestApiProvider + apis={[ + [entityFeedbackApiRef, feedbackApi], + [errorApiRef, errorApi], + [identityApiRef, identityApi], + ]} + > + <AsyncEntityProvider loading={false} entity={testEntity}> + <StarredRatingButtons {...props} /> + </AsyncEntityProvider> + </TestApiProvider>, + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('loads the previous rating if it exists', async () => { + await render(); + expect(feedbackApi.getRatings).toHaveBeenCalledWith( + 'component:default/test', + ); + }); + + it('applies a rating correctly', async () => { + const rendered = await render(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-3'), + ); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.three.toString(), + ); + + jest.clearAllMocks(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-5'), + ); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.five.toString(), + ); + }); + + it('ignores an existing rating correctly', async () => { + const rendered = await render(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-2'), + ); + expect(feedbackApi.recordRating).not.toHaveBeenCalled(); + }); + + it('opens a response dialog if a rating under the threshold is selected', async () => { + const rendered = await render(); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-1'), + ); + expect(rendered.getByText('dialog is open')).toBeInTheDocument(); + }); + + it('opens a response dialog if a rating under a custom threshold is selected', async () => { + const rendered = await render({ requestResponseThreshold: 4 }); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-3'), + ); + expect(rendered.getByText('dialog is open')).toBeInTheDocument(); + }); + + it('does not open a response dialog if configured not to', async () => { + const rendered = await render({ requestResponse: false }); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-1'), + ); + expect(rendered.queryByText('dialog is open')).toBeNull(); + }); +}); diff --git a/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.tsx b/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.tsx new file mode 100644 index 0000000000..51f7da1099 --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.tsx @@ -0,0 +1,160 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Progress } from '@backstage/core-components'; +import { + ErrorApiError, + errorApiRef, + identityApiRef, + useApi, +} from '@backstage/core-plugin-api'; +import { useAsyncEntity } from '@backstage/plugin-catalog-react'; +import { IconButton } from '@material-ui/core'; +import StarOutlineIcon from '@material-ui/icons/StarOutline'; +import StarIcon from '@material-ui/icons/Star'; +import React, { ReactNode, useCallback, useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +import { entityFeedbackApiRef } from '../../api'; +import { + EntityFeedbackResponse, + FeedbackResponseDialog, +} from '../FeedbackResponseDialog'; + +export enum FeedbackRatings { + one = 1, + two = 2, + three = 3, + four = 4, + five = 5, +} + +/** + * @public + */ +export interface StarredRatingButtonsProps { + feedbackDialogResponses?: EntityFeedbackResponse[]; + feedbackDialogTitle?: ReactNode; + requestResponse?: boolean; + requestResponseThreshold?: number; +} + +export const StarredRatingButtons = (props: StarredRatingButtonsProps) => { + const { + feedbackDialogResponses, + feedbackDialogTitle, + requestResponse = true, + requestResponseThreshold = FeedbackRatings.two, + } = props; + const errorApi = useApi(errorApiRef); + const feedbackApi = useApi(entityFeedbackApiRef); + const identityApi = useApi(identityApiRef); + const [rating, setRating] = useState<FeedbackRatings>(); + const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false); + const { entity, loading: loadingEntity } = useAsyncEntity(); + + const { loading: loadingFeedback } = useAsync(async () => { + // Wait until entity is loaded + if (!entity) { + return; + } + + try { + const identity = await identityApi.getBackstageIdentity(); + const prevFeedback = await feedbackApi.getRatings( + stringifyEntityRef(entity), + ); + + const prevRating = prevFeedback.find( + r => r.userRef === identity.userEntityRef, + )?.rating; + if (prevRating) { + setRating(parseInt(prevRating, 10)); + } + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, [entity, feedbackApi, setRating]); + + const [{ loading: savingFeedback }, saveFeedback] = useAsyncFn( + async (feedback: FeedbackRatings) => { + try { + await feedbackApi.recordRating( + stringifyEntityRef(entity!), + feedback.toString(), + ); + setRating(feedback); + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, + [entity, feedbackApi, setRating], + ); + + const applyRating = useCallback( + (feedback: FeedbackRatings) => { + // Ignore rating if feedback is same as current + if (feedback === rating) { + return; + } + + saveFeedback(feedback); + if (feedback <= requestResponseThreshold && requestResponse) { + setOpenFeedbackDialog(true); + } + }, + [ + rating, + requestResponse, + requestResponseThreshold, + saveFeedback, + setOpenFeedbackDialog, + ], + ); + + if (loadingEntity || loadingFeedback || savingFeedback) { + return <Progress />; + } + + return ( + <> + {Object.values(FeedbackRatings) + .filter(o => typeof o === 'number') + .map(starRating => ( + <IconButton + key={starRating} + data-testid={`entity-feedback-star-button-${starRating}`} + onClick={() => applyRating(starRating as FeedbackRatings)} + > + {rating && rating >= starRating ? ( + <StarIcon fontSize="small" /> + ) : ( + <StarOutlineIcon fontSize="small" /> + )} + </IconButton> + ))} + <FeedbackResponseDialog + entity={entity!} + open={openFeedbackDialog} + onClose={() => setOpenFeedbackDialog(false)} + feedbackDialogResponses={feedbackDialogResponses} + feedbackDialogTitle={feedbackDialogTitle} + /> + </> + ); +}; diff --git a/plugins/entity-feedback/src/components/StarredRatingButtons/index.ts b/plugins/entity-feedback/src/components/StarredRatingButtons/index.ts new file mode 100644 index 0000000000..659c914749 --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingButtons/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './StarredRatingButtons'; diff --git a/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.test.tsx b/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.test.tsx new file mode 100644 index 0000000000..be53d59d5f --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; + +import { FeedbackRatings } from '../StarredRatingButtons'; +import { StarredRatingsTable } from './StarredRatingsTable'; + +jest.mock('../FeedbackRatingsTable', () => ({ + FeedbackRatingsTable: ({ ratingValues }: { ratingValues: string[] }) => { + return ( + <span> + {ratingValues.map(v => ( + <span key={v}>{v}</span> + ))} + </span> + ); + }, +})); + +describe('StarredRatingsTable', () => { + it('renders starred ratings correctly', async () => { + const rendered = await renderInTestApp(<StarredRatingsTable />); + expect(rendered.getByText(FeedbackRatings.one)).toBeInTheDocument(); + expect(rendered.getByText(FeedbackRatings.two)).toBeInTheDocument(); + expect(rendered.getByText(FeedbackRatings.three)).toBeInTheDocument(); + expect(rendered.getByText(FeedbackRatings.four)).toBeInTheDocument(); + expect(rendered.getByText(FeedbackRatings.five)).toBeInTheDocument(); + }); +}); diff --git a/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.tsx b/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.tsx new file mode 100644 index 0000000000..d52fd64640 --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import { FeedbackRatingsTable } from '../FeedbackRatingsTable'; +import { FeedbackRatings } from '../StarredRatingButtons'; + +/** + * @public + */ +export interface StarredRatingsTableProps { + allEntities?: boolean; + ownerRef?: string; + title?: string; +} + +export const StarredRatingsTable = (props: StarredRatingsTableProps) => { + const { allEntities, ownerRef, title } = props; + + return ( + <FeedbackRatingsTable + allEntities={allEntities} + ownerRef={ownerRef} + ratingValues={Object.values(FeedbackRatings) + .filter(o => typeof o === 'number') + .map(r => r.toString())} + title={title} + /> + ); +}; diff --git a/plugins/entity-feedback/src/components/StarredRatingsTable/index.ts b/plugins/entity-feedback/src/components/StarredRatingsTable/index.ts new file mode 100644 index 0000000000..9621712e0b --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingsTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './StarredRatingsTable'; diff --git a/plugins/entity-feedback/src/components/index.ts b/plugins/entity-feedback/src/components/index.ts new file mode 100644 index 0000000000..a88d525f09 --- /dev/null +++ b/plugins/entity-feedback/src/components/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { + EntityFeedbackResponse, + FeedbackResponseDialogProps, +} from './FeedbackResponseDialog'; +export type { FeedbackResponseTableProps } from './FeedbackResponseTable'; +export type { LikeDislikeButtonsProps } from './LikeDislikeButtons'; +export type { LikeDislikeRatingsTableProps } from './LikeDislikeRatingsTable'; +export type { StarredRatingButtonsProps } from './StarredRatingButtons'; +export type { StarredRatingsTableProps } from './StarredRatingsTable'; diff --git a/plugins/entity-feedback/src/index.ts b/plugins/entity-feedback/src/index.ts new file mode 100644 index 0000000000..bbcf6bc626 --- /dev/null +++ b/plugins/entity-feedback/src/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Entity Feedback frontend plugin + * + * @packageDocumentation + */ +export type { + LikeDislikeButtonsProps, + LikeDislikeRatingsTableProps, + EntityFeedbackResponse, + FeedbackResponseDialogProps, + FeedbackResponseTableProps, + StarredRatingButtonsProps, + StarredRatingsTableProps, +} from './components'; +export * from './plugin'; +export * from './api'; diff --git a/plugins/entity-feedback/src/plugin.test.ts b/plugins/entity-feedback/src/plugin.test.ts new file mode 100644 index 0000000000..a56d37f9ab --- /dev/null +++ b/plugins/entity-feedback/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { entityFeedbackPlugin } from './plugin'; + +describe('entity-feedback', () => { + it('should export plugin', () => { + expect(entityFeedbackPlugin).toBeDefined(); + }); +}); diff --git a/plugins/entity-feedback/src/plugin.tsx b/plugins/entity-feedback/src/plugin.tsx new file mode 100644 index 0000000000..9ebdc5d7af --- /dev/null +++ b/plugins/entity-feedback/src/plugin.tsx @@ -0,0 +1,212 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { + createApiFactory, + createComponentExtension, + createPlugin, + createRoutableExtension, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; +import { useAsyncEntity } from '@backstage/plugin-catalog-react'; +import React from 'react'; + +import { entityFeedbackApiRef, EntityFeedbackClient } from './api'; +import { rootRouteRef } from './routes'; + +/** + * @public + */ +export const entityFeedbackPlugin = createPlugin({ + id: 'entity-feedback', + routes: { + root: rootRouteRef, + }, + apis: [ + createApiFactory({ + api: entityFeedbackApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new EntityFeedbackClient({ discoveryApi, fetchApi }), + }), + ], +}); + +/** + * @public + */ +export const LikeDislikeButtons = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'LikeDislikeButtons', + component: { + lazy: () => + import('./components/LikeDislikeButtons').then( + m => m.LikeDislikeButtons, + ), + }, + }), +); + +/** + * @public + */ +export const StarredRatingButtons = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'StarredRatingButtons', + component: { + lazy: () => + import('./components/StarredRatingButtons').then( + m => m.StarredRatingButtons, + ), + }, + }), +); + +/** + * @public + */ +export const FeedbackResponseDialog = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'FeedbackResponseDialog', + component: { + lazy: () => + import('./components/FeedbackResponseDialog').then( + m => m.FeedbackResponseDialog, + ), + }, + }), +); + +/** + * @public + */ +export const EntityFeedbackResponseContent = entityFeedbackPlugin.provide( + createRoutableExtension({ + name: 'EntityFeedbackResponseContent', + mountPoint: rootRouteRef, + component: () => + import('./components/FeedbackResponseTable').then( + ({ FeedbackResponseTable }) => { + return () => { + const { entity } = useAsyncEntity(); + return ( + <FeedbackResponseTable + entityRef={entity ? stringifyEntityRef(entity) : ''} + /> + ); + }; + }, + ), + }), +); + +/** + * @public + */ +export const FeedbackResponseTable = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'FeedbackResponseTable', + component: { + lazy: () => + import('./components/FeedbackResponseTable').then( + m => m.FeedbackResponseTable, + ), + }, + }), +); + +/** + * @public + */ +export const EntityLikeDislikeRatingsCard = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'EntityLikeDislikeRatingsCard', + component: { + lazy: () => + import('./components/LikeDislikeRatingsTable').then( + ({ LikeDislikeRatingsTable }) => { + return () => { + const { entity } = useAsyncEntity(); + return ( + <LikeDislikeRatingsTable + ownerRef={entity ? stringifyEntityRef(entity) : ''} + /> + ); + }; + }, + ), + }, + }), +); + +/** + * @public + */ +export const LikeDislikeRatingsTable = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'LikeDislikeRatingsTable', + component: { + lazy: () => + import('./components/LikeDislikeRatingsTable').then( + m => m.LikeDislikeRatingsTable, + ), + }, + }), +); + +/** + * @public + */ +export const EntityStarredRatingsCard = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'EntityStarredRatingsCard', + component: { + lazy: () => + import('./components/StarredRatingsTable').then( + ({ StarredRatingsTable }) => { + return () => { + const { entity } = useAsyncEntity(); + return ( + <StarredRatingsTable + ownerRef={entity ? stringifyEntityRef(entity) : ''} + /> + ); + }; + }, + ), + }, + }), +); + +/** + * @public + */ +export const StarredRatingsTable = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'StarredRatingsTable', + component: { + lazy: () => + import('./components/StarredRatingsTable').then( + m => m.StarredRatingsTable, + ), + }, + }), +); diff --git a/plugins/entity-feedback/src/routes.ts b/plugins/entity-feedback/src/routes.ts new file mode 100644 index 0000000000..cde7241931 --- /dev/null +++ b/plugins/entity-feedback/src/routes.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'entity-feedback', +}); diff --git a/plugins/entity-feedback/src/setupTests.ts b/plugins/entity-feedback/src/setupTests.ts new file mode 100644 index 0000000000..bf4855cb6e --- /dev/null +++ b/plugins/entity-feedback/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/entity-validation/.eslintrc.js b/plugins/entity-validation/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/entity-validation/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/entity-validation/CHANGELOG.md b/plugins/entity-validation/CHANGELOG.md new file mode 100644 index 0000000000..6b80724f92 --- /dev/null +++ b/plugins/entity-validation/CHANGELOG.md @@ -0,0 +1,51 @@ +# @backstage/plugin-entity-validation + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.1.0 + +### Minor Changes + +- d34b3267f9: First implementation for the entity validation UI. For more information refer to its `README.md`. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-common@1.0.11 + +## 0.1.0-next.0 + +### Minor Changes + +- d34b3267f9: First implementation for the entity validation UI. For more information refer to its `README.md`. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 diff --git a/plugins/entity-validation/README.md b/plugins/entity-validation/README.md new file mode 100644 index 0000000000..1f864d0857 --- /dev/null +++ b/plugins/entity-validation/README.md @@ -0,0 +1,53 @@ +# entity-validation + +This plugin creates a new page in Backstage where the user can validate the entities. + +![Entity Validation](./docs/entity-validation.png) + +## Getting started + +First of all, install the package in the `app` package by running the following command: + +```bash +yarn add --cwd packages/app @backstage/plugin-entity-validation +``` + +Add the new route to the app by adding the following line: + +```typescript +// In packages/app/src/App.tsx +import { EntityValidationPage } from '@backstage/plugin-entity-validation'; + +const routes = ( + <FlatRoutes> + {/* ...other routes */} + <Route path="/entity-validation" element={<EntityValidationPage />} /> + </FlatRoutes> +); +``` + +To add the new page to your sidebar, you must include these lines in your `Root.tsx` file: + +```typescript + // In packages/app/src/components/Root/Root.tsx + import BuildIcon from '@material-ui/icons/Build'; + + ... + + export const Root = ({ children }: PropsWithChildren<{}>) => ( + <SidebarPage> + <Sidebar> + {/* ...other elements */} + <SidebarGroup label="Menu" icon={<MenuIcon />}> + {/* Global nav, not org-specific */} + {/* ...other sidebars */} + <SidebarItem icon={BuildIcon} to="entity-validation" text="Validator" /> + {/* End global nav */} + <SidebarDivider /> + </SidebarGroup> + {/* ...other elements */} + </Sidebar> + {children} + </SidebarPage> + ); +``` diff --git a/plugins/entity-validation/api-report.md b/plugins/entity-validation/api-report.md new file mode 100644 index 0000000000..2fccbf9471 --- /dev/null +++ b/plugins/entity-validation/api-report.md @@ -0,0 +1,27 @@ +## API Report File for "@backstage/plugin-entity-validation" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="react" /> + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const EntityValidationPage: (props: { + defaultYaml?: string | undefined; + defaultLocation?: string | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const entityValidationPlugin: BackstagePlugin< + { + root: RouteRef<undefined>; + }, + {}, + {} +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/entity-validation/dev/index.tsx b/plugins/entity-validation/dev/index.tsx new file mode 100644 index 0000000000..42f4b152ed --- /dev/null +++ b/plugins/entity-validation/dev/index.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { entityValidationPlugin, EntityValidationPage } from '../src/plugin'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; + +createDevApp() + .registerPlugin(entityValidationPlugin) + .registerApi({ + api: catalogApiRef, + deps: {}, + factory: () => + ({ + getEntities: () => ({}), + } as CatalogApi), + }) + .addPage({ + element: <EntityValidationPage />, + title: 'Root Page', + path: '/entity-validation', + }) + .render(); diff --git a/plugins/entity-validation/docs/entity-validation.png b/plugins/entity-validation/docs/entity-validation.png new file mode 100644 index 0000000000..00cb5fafe0 Binary files /dev/null and b/plugins/entity-validation/docs/entity-validation.png differ diff --git a/plugins/entity-validation/package.json b/plugins/entity-validation/package.json new file mode 100644 index 0000000000..5d509f1eea --- /dev/null +++ b/plugins/entity-validation/package.json @@ -0,0 +1,68 @@ +{ + "name": "@backstage/plugin-entity-validation", + "version": "0.1.1-next.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/entity-validation" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/theme": "workspace:^", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.1.0", + "@codemirror/view": "^6.0.0", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "@react-hookz/web": "^20.0.0", + "@uiw/react-codemirror": "^4.9.3", + "lodash": "^4.17.21", + "react-use": "^17.2.4", + "yaml": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx b/plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx new file mode 100644 index 0000000000..c39f8d5ed7 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx @@ -0,0 +1,97 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useMemo, useState } from 'react'; +import { Box, makeStyles } from '@material-ui/core'; +import CodeMirror from '@uiw/react-codemirror'; +import { showPanel } from '@codemirror/view'; +import { StreamLanguage } from '@codemirror/language'; +import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; +import { useKeyboardEvent } from '@react-hookz/web'; + +const useStyles = makeStyles(theme => ({ + container: { + position: 'relative', + width: '100%', + height: '100%', + minHeight: '400px', + }, + codeMirror: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + infoPanel: { + color: theme.palette.info.main, + lineHeight: 2, + margin: theme.spacing(0, 1), + }, +})); + +type TemplateTextAreaProps = { + catalogYaml: string; + onChange: (value: string) => void; + onValidate: () => void; +}; + +export const EntityTextArea = ({ + catalogYaml, + onChange, + onValidate, +}: TemplateTextAreaProps) => { + const classes = useStyles(); + const [close, setClose] = useState(false); + + const panelExtension = useMemo(() => { + if (close) { + return showPanel.of(null); + } + + const dom = document.createElement('div'); + dom.classList.add(classes.infoPanel); + dom.textContent = + 'To validate your catalog-info.yaml click on the "Validate" button or use "Ctrl + S" or "Ctrl + Enter"'; + dom.onclick = () => setClose(true); + return showPanel.of(() => ({ dom, top: true })); + }, [classes, close]); + + // Triggers a validation when Ctrl+S or Ctrl+Enter instead of default behavior + useKeyboardEvent( + e => (e.key === 's' || e.key === 'Enter') && (e.ctrlKey || e.metaKey), + e => { + e.preventDefault(); + onValidate(); + }, + ); + + return ( + <Box className={classes.container}> + <CodeMirror + className={classes.codeMirror} + theme="dark" + height="100%" + extensions={[StreamLanguage.define(yamlSupport), panelExtension]} + value={catalogYaml} + onChange={onChange} + onKeyDownCapture={e => { + // Prevent new line if Ctrl + Enter was clicked + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) e.preventDefault(); + }} + /> + </Box> + ); +}; diff --git a/plugins/entity-validation/src/components/EntityTextArea/index.ts b/plugins/entity-validation/src/components/EntityTextArea/index.ts new file mode 100644 index 0000000000..d2a5e41d0f --- /dev/null +++ b/plugins/entity-validation/src/components/EntityTextArea/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './EntityTextArea'; diff --git a/plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx b/plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx new file mode 100644 index 0000000000..85d47fa2c2 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; +import { ValidateEntityResponse } from '@backstage/catalog-client'; +import { useApp } from '@backstage/core-plugin-api'; +import { + Collapse, + IconButton, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + makeStyles, + Paper, +} from '@material-ui/core'; +import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; +import WorkIcon from '@material-ui/icons/Work'; +import ExpandLessIcon from '@material-ui/icons/ExpandLess'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { MarkdownContent } from '@backstage/core-components'; +import { ValidationOutputOk } from '../../types'; + +const useStyles = makeStyles(theme => ({ + validationOk: { + color: theme.palette.success.main, + }, + validationNotOk: { + color: theme.palette.error.main, + }, + errorContainer: { + color: theme.palette.error.main, + background: theme.palette.background.paper, + padding: theme.spacing(2), + }, +})); + +type EntityResultProps = { + isFirstError?: boolean; + item: ValidationOutputOk; +}; + +export const EntityResult = ({ + isFirstError = false, + item, +}: EntityResultProps) => { + const classes = useStyles(); + const app = useApp(); + const [expanded, setExpanded] = useState(isFirstError); + + const Icon = + app.getSystemIcon(`kind:${item.entity.kind.toLocaleLowerCase('en-US')}`) ?? + WorkIcon; + + const fetchErrorMessages = (response: ValidateEntityResponse) => { + if (!response.valid) { + return response.errors.map(err => err.message).join('\n\n'); + } + return ''; + }; + + return ( + <> + <ListItem key={humanizeEntityRef(item.entity)}> + <ListItemIcon> + <Icon + className={ + item.response.valid + ? classes.validationOk + : classes.validationNotOk + } + /> + </ListItemIcon> + <ListItemText + primary={humanizeEntityRef(item.entity)} + onClick={() => setExpanded(!expanded)} + /> + {!item.response.valid && ( + <ListItemSecondaryAction> + <IconButton edge="end" onClick={() => setExpanded(!expanded)}> + {expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />} + </IconButton> + </ListItemSecondaryAction> + )} + </ListItem> + {!item.response.valid && ( + <Collapse in={expanded} timeout="auto" unmountOnExit> + <Paper className={classes.errorContainer}> + <MarkdownContent content={fetchErrorMessages(item.response)} /> + </Paper> + </Collapse> + )} + </> + ); +}; diff --git a/plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx b/plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx new file mode 100644 index 0000000000..bc7e22ef54 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx @@ -0,0 +1,162 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import { + catalogApiRef, + humanizeEntityRef, +} from '@backstage/plugin-catalog-react'; +import { List, makeStyles, Typography } from '@material-ui/core'; +import useAsync from 'react-use/lib/useAsync'; +import { InfoCard, Progress } from '@backstage/core-components'; +import Alert from '@material-ui/lab/Alert'; +import { + CatalogProcessorResult, + ValidationOutput, + ValidationOutputError, + ValidationOutputOk, +} from '../../types'; +import { EntityResult } from './EntityResult'; + +const useStyles = makeStyles(theme => ({ + validationOk: { + color: theme.palette.success.main, + }, + validationNotOk: { + color: theme.palette.error.main, + }, +})); + +function sortResults(items: Array<ValidationOutputOk>) { + return items.sort((a, b) => + humanizeEntityRef(a.entity).localeCompare(humanizeEntityRef(b.entity)), + ); +} + +type EntityValidationOutputProps = { + processorResults?: CatalogProcessorResult[]; + locationUrl: string; +}; + +export const EntityValidationOutput = ({ + processorResults, + locationUrl, +}: EntityValidationOutputProps) => { + const classes = useStyles(); + const catalogApi = useApi(catalogApiRef); + const identityApi = useApi(identityApiRef); + + const { value = [], loading } = useAsync(async (): Promise< + ValidationOutput[] | undefined + > => { + if (!processorResults) { + return undefined; + } + + const location = locationUrl + ? `url:${locationUrl}` + : 'url:http://localhost'; + + return await Promise.all( + processorResults.map(async item => { + if (item.type === 'entity') { + const { token } = await identityApi.getCredentials(); + + const validateResult = await catalogApi.validateEntity( + item.entity, + location, + { + token, + }, + ); + return { + type: 'valid', + entity: item.entity, + response: validateResult, + }; + } else if (item.type === 'error') { + return { + type: 'error', + processingError: `Malformed entity YAML, ${item.error}`, + }; + } + return { + type: 'error', + processingError: `Internal error, failed to parse entity`, + }; + }), + ); + }, [processorResults]); + + if (loading) { + return <Progress />; + } + + const errors = value.filter( + v => v.type === 'error', + ) as ValidationOutputError[]; + + const results = sortResults( + value.filter(v => v.type === 'valid') as ValidationOutputOk[], + ); + // If there are any errors, only the first one will be expanded + const firstErrorIndex = results.findIndex(r => !r.response.valid); + + if (errors.length !== 0) { + return ( + <> + {errors.map(err => ( + <Alert severity="error">{err.processingError}</Alert> + ))} + </> + ); + } + + return ( + <InfoCard> + {results.length === 0 ? ( + <div style={{ padding: '15px' }}> + <Typography variant="body1"> + No entity definitions found or validated yet + </Typography> + </div> + ) : ( + <> + <List disablePadding dense> + {results.map((item, key) => ( + <EntityResult + key={key} + item={item} + isFirstError={key === firstErrorIndex} + /> + ))} + </List> + <div style={{ marginTop: '25px', textAlign: 'center' }}> + {results.every(r => r.response.valid) ? ( + <Typography variant="body1" className={classes.validationOk}> + All the entities are valid! + </Typography> + ) : ( + <Typography variant="body1" className={classes.validationNotOk}> + One or more entities have validation errors + </Typography> + )} + </div> + </> + )} + </InfoCard> + ); +}; diff --git a/plugins/entity-validation/src/components/EntityValidationOutput/index.ts b/plugins/entity-validation/src/components/EntityValidationOutput/index.ts new file mode 100644 index 0000000000..8d327581c6 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationOutput/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './EntityValidationOutput'; +export * from './EntityResult'; diff --git a/plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx b/plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx new file mode 100644 index 0000000000..0f0e60366b --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState } from 'react'; +import { Content, Header, LinkButton, Page } from '@backstage/core-components'; +import { EntityTextArea } from '../EntityTextArea'; +import { Grid, TextField } from '@material-ui/core'; +import { CatalogProcessorResult } from '../../types'; +import { parseEntityYaml } from '../../utils'; +import { EntityValidationOutput } from '../EntityValidationOutput'; + +const EXAMPLE_CATALOG_INFO_YAML = `# Put your catalog-info.yaml below and validate it +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test + description: Component description + links: [] + tags: [] + annotations: {} +spec: + type: service + lifecycle: experimental + owner: owner +`; + +export const EntityValidationPage = (props: { + defaultYaml?: string; + defaultLocation?: string; +}) => { + const { + defaultYaml = EXAMPLE_CATALOG_INFO_YAML, + defaultLocation = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + } = props; + + const [catalogYaml, setCatalogYaml] = useState(defaultYaml); + const [yamlFiles, setYamlFiles] = useState<CatalogProcessorResult[]>(); + const [locationUrl, setLocationUrl] = useState(defaultLocation); + + const parseYaml = () => { + const parsedFiles = [ + ...parseEntityYaml(Buffer.from(catalogYaml), { + type: 'url', + target: locationUrl ? locationUrl : 'http://localhost', + }), + ]; + setYamlFiles(parsedFiles); + }; + + return ( + <Page themeId="tool"> + <Header + title="Entity Validator" + subtitle="Tool to validate catalog-info.yaml files" + /> + <Content> + <Grid container> + <Grid item md={9} xs={12}> + <TextField + fullWidth + label="File Location" + margin="normal" + variant="outlined" + required + value={locationUrl} + placeholder={defaultLocation} + helperText="Location where you catalog-info.yaml file is, or will be, located" + onChange={e => setLocationUrl(e.target.value)} + /> + </Grid> + <Grid item md={3} xs={12}> + <Grid container alignItems="center" style={{ height: '100%' }}> + <Grid item> + <LinkButton size="large" onClick={parseYaml} to="#"> + Validate + </LinkButton> + </Grid> + </Grid> + </Grid> + </Grid> + <Grid container direction="row" style={{ height: '90%' }}> + <Grid item md={6} xs={12}> + <EntityTextArea + onValidate={parseYaml} + onChange={(value: string) => setCatalogYaml(value)} + catalogYaml={catalogYaml} + /> + </Grid> + <Grid item md={6} xs={12}> + <Grid container spacing={3}> + <Grid item xs={12}> + <EntityValidationOutput + processorResults={yamlFiles} + locationUrl={locationUrl} + /> + </Grid> + </Grid> + </Grid> + </Grid> + </Content> + </Page> + ); +}; diff --git a/plugins/entity-validation/src/components/EntityValidationPage/index.ts b/plugins/entity-validation/src/components/EntityValidationPage/index.ts new file mode 100644 index 0000000000..24149a9387 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './EntityValidationPage'; diff --git a/plugins/entity-validation/src/index.ts b/plugins/entity-validation/src/index.ts new file mode 100644 index 0000000000..33ae575dfe --- /dev/null +++ b/plugins/entity-validation/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { entityValidationPlugin, EntityValidationPage } from './plugin'; diff --git a/plugins/entity-validation/src/plugin.test.ts b/plugins/entity-validation/src/plugin.test.ts new file mode 100644 index 0000000000..eae69e8d3a --- /dev/null +++ b/plugins/entity-validation/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { entityValidationPlugin } from './plugin'; + +describe('entity-validation', () => { + it('should export plugin', () => { + expect(entityValidationPlugin).toBeDefined(); + }); +}); diff --git a/plugins/entity-validation/src/plugin.ts b/plugins/entity-validation/src/plugin.ts new file mode 100644 index 0000000000..9a3014da28 --- /dev/null +++ b/plugins/entity-validation/src/plugin.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createComponentExtension, + createPlugin, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +/** @public */ +export const entityValidationPlugin = createPlugin({ + id: 'entity-validation', + routes: { + root: rootRouteRef, + }, +}); + +/** @public */ +export const EntityValidationPage = entityValidationPlugin.provide( + createComponentExtension({ + name: 'EntityValidationPage', + component: { + lazy: () => + import('./components/EntityValidationPage').then( + m => m.EntityValidationPage, + ), + }, + }), +); diff --git a/plugins/entity-validation/src/processingResult.ts b/plugins/entity-validation/src/processingResult.ts new file mode 100644 index 0000000000..de9d8adbbd --- /dev/null +++ b/plugins/entity-validation/src/processingResult.ts @@ -0,0 +1,73 @@ +/* + * 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. + */ + +// File copied from https://github.com/backstage/backstage/blob/master/plugins/catalog-node/src/api/processingResult.ts +import { InputError, NotFoundError } from '@backstage/errors'; +import { Entity } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { CatalogProcessorResult, EntityRelationSpec } from './types'; + +/** + * 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): CatalogProcessorResult { + return { type: 'location', location: newLocation }; + }, + + entity(atLocation: LocationSpec, newEntity: Entity): CatalogProcessorResult { + return { type: 'entity', location: atLocation, entity: newEntity }; + }, + + relation(spec: EntityRelationSpec): CatalogProcessorResult { + return { type: 'relation', relation: spec }; + }, + + refresh(key: string): CatalogProcessorResult { + return { type: 'refresh', key }; + }, +} as const); diff --git a/plugins/entity-validation/src/routes.ts b/plugins/entity-validation/src/routes.ts new file mode 100644 index 0000000000..8cdc138782 --- /dev/null +++ b/plugins/entity-validation/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'entity-validation', +}); diff --git a/plugins/entity-validation/src/setupTests.ts b/plugins/entity-validation/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/entity-validation/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/entity-validation/src/types.ts b/plugins/entity-validation/src/types.ts new file mode 100644 index 0000000000..7ecc27bb25 --- /dev/null +++ b/plugins/entity-validation/src/types.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import { ValidateEntityResponse } from '@backstage/catalog-client'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; + +export type ValidationOutputError = { + type: 'error'; + processingError: string; +}; + +export type ValidationOutputOk = { + type: 'valid'; + entity: Entity; + response: ValidateEntityResponse; +}; + +export type ValidationOutput = ValidationOutputOk | ValidationOutputError; + +export type EntityRelationSpec = { + /** + * The source entity of this relation. + */ + source: CompoundEntityRef; + + /** + * The type of the relation. + */ + type: string; + + /** + * The target entity of this relation. + */ + target: CompoundEntityRef; +}; + +type CatalogProcessorLocationResult = { + type: 'location'; + location: LocationSpec; +}; + +type CatalogProcessorEntityResult = { + type: 'entity'; + entity: Entity; + location: LocationSpec; +}; + +type CatalogProcessorRelationResult = { + type: 'relation'; + relation: EntityRelationSpec; +}; + +type CatalogProcessorErrorResult = { + type: 'error'; + error: Error; + location: LocationSpec; +}; + +type CatalogProcessorRefreshKeysResult = { + type: 'refresh'; + key: string; +}; + +export type CatalogProcessorResult = + | CatalogProcessorLocationResult + | CatalogProcessorEntityResult + | CatalogProcessorRelationResult + | CatalogProcessorErrorResult + | CatalogProcessorRefreshKeysResult; diff --git a/plugins/entity-validation/src/utils.ts b/plugins/entity-validation/src/utils.ts new file mode 100644 index 0000000000..d81de6f6c9 --- /dev/null +++ b/plugins/entity-validation/src/utils.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Entity, stringifyLocationRef } from '@backstage/catalog-model'; +import lodash from 'lodash'; +import yaml from 'yaml'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { CatalogProcessorResult } from './types'; +import { processingResult } from './processingResult'; + +// Copy from: https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/modules/util/parse.ts +export function* parseEntityYaml( + data: Buffer, + location: LocationSpec, +): Iterable<CatalogProcessorResult> { + let documents: yaml.Document.Parsed[]; + try { + documents = yaml.parseAllDocuments(data.toString('utf8')).filter(d => d); + } catch (e) { + const loc = stringifyLocationRef(location); + const message = `Failed to parse YAML at ${loc}, ${e}`; + yield processingResult.generalError(location, message); + return; + } + + for (const document of documents) { + if (document.errors?.length) { + const loc = stringifyLocationRef(location); + const message = `YAML error at ${loc}, ${document.errors[0]}`; + yield processingResult.generalError(location, message); + } else { + const json = document.toJSON(); + if (lodash.isPlainObject(json)) { + 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 processingResult.generalError(location, message); + } + } + } +} diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 6fc92e2700..af976da894 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/plugin-events-node@0.2.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/alpha-api-report.md b/plugins/events-backend-module-aws-sqs/alpha-api-report.md new file mode 100644 index 0000000000..b9f60c4437 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-events-backend-module-aws-sqs" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const awsSqsConsumingEventPublisherEventsModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-backend-module-aws-sqs/api-report.md b/plugins/events-backend-module-aws-sqs/api-report.md index ede18f2787..ffa9c888d6 100644 --- a/plugins/events-backend-module-aws-sqs/api-report.md +++ b/plugins/events-backend-module-aws-sqs/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; @@ -21,7 +20,4 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { // (undocumented) setEventBroker(eventBroker: EventBroker): Promise<void>; } - -// @alpha -export const awsSqsConsumingEventPublisherEventsModule: () => BackendFeature; ``` diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 323b015237..53901e5038 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,21 +1,33 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -42,7 +54,6 @@ "aws-sdk-client-mock": "^2.0.0" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/events-backend-module-aws-sqs/src/alpha.ts b/plugins/events-backend-module-aws-sqs/src/alpha.ts new file mode 100644 index 0000000000..00f08e4417 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { awsSqsConsumingEventPublisherEventsModule } from './service/AwsSqsConsumingEventPublisherEventsModule'; diff --git a/plugins/events-backend-module-aws-sqs/src/index.ts b/plugins/events-backend-module-aws-sqs/src/index.ts index 73b950856b..57f409c044 100644 --- a/plugins/events-backend-module-aws-sqs/src/index.ts +++ b/plugins/events-backend-module-aws-sqs/src/index.ts @@ -24,4 +24,3 @@ */ export { AwsSqsConsumingEventPublisher } from './publisher/AwsSqsConsumingEventPublisher'; -export { awsSqsConsumingEventPublisherEventsModule } from './service/AwsSqsConsumingEventPublisherEventsModule'; diff --git a/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.test.ts b/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.test.ts index 589b8c93fa..3963b9a8b6 100644 --- a/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.test.ts +++ b/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.test.ts @@ -18,7 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { coreServices } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; import { awsSqsConsumingEventPublisherEventsModule } from './AwsSqsConsumingEventPublisherEventsModule'; import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; diff --git a/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.ts b/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.ts index 50812f321b..4205bd231b 100644 --- a/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.ts +++ b/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.ts @@ -19,7 +19,7 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; /** diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 1e4c666332..54c0f53713 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-events-backend-module-azure +## 0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/events-backend-module-azure/alpha-api-report.md b/plugins/events-backend-module-azure/alpha-api-report.md new file mode 100644 index 0000000000..e3fed2cab0 --- /dev/null +++ b/plugins/events-backend-module-azure/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-events-backend-module-azure" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const azureDevOpsEventRouterEventsModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-backend-module-azure/api-report.md b/plugins/events-backend-module-azure/api-report.md index 2e13a7ed4a..4460aeb510 100644 --- a/plugins/events-backend-module-azure/api-report.md +++ b/plugins/events-backend-module-azure/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { EventParams } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -13,7 +12,4 @@ export class AzureDevOpsEventRouter extends SubTopicEventRouter { // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } - -// @alpha -export const azureDevOpsEventRouterEventsModule: () => BackendFeature; ``` diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 4e5c429b39..bdfa37d36f 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,21 +1,33 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -34,7 +46,6 @@ "supertest": "^6.1.3" }, "files": [ - "alpha", "dist" ] } diff --git a/plugins/events-backend-module-azure/src/alpha.ts b/plugins/events-backend-module-azure/src/alpha.ts new file mode 100644 index 0000000000..8eacbd8270 --- /dev/null +++ b/plugins/events-backend-module-azure/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { azureDevOpsEventRouterEventsModule } from './service/AzureDevOpsEventRouterEventsModule'; diff --git a/plugins/events-backend-module-azure/src/index.ts b/plugins/events-backend-module-azure/src/index.ts index 9a83f751f6..a1d56dc536 100644 --- a/plugins/events-backend-module-azure/src/index.ts +++ b/plugins/events-backend-module-azure/src/index.ts @@ -22,4 +22,3 @@ */ export { AzureDevOpsEventRouter } from './router/AzureDevOpsEventRouter'; -export { azureDevOpsEventRouterEventsModule } from './service/AzureDevOpsEventRouterEventsModule'; diff --git a/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.test.ts b/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.test.ts index 86ab7ea17d..dbd61782ae 100644 --- a/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.test.ts +++ b/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.test.ts @@ -15,7 +15,7 @@ */ import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { azureDevOpsEventRouterEventsModule } from './AzureDevOpsEventRouterEventsModule'; import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; diff --git a/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.ts b/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.ts index 1219452bd8..18851befa4 100644 --- a/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.ts +++ b/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.ts @@ -15,13 +15,13 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; /** * Module for the events-backend plugin, adding an event router for Azure DevOps. * - * Registers the {@link AzureDevOpsEventRouter}. + * Registers the `AzureDevOpsEventRouter`. * * @alpha */ diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index bf95401831..2310a2eb44 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/alpha-api-report.md b/plugins/events-backend-module-bitbucket-cloud/alpha-api-report.md new file mode 100644 index 0000000000..f8e550bf98 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-events-backend-module-bitbucket-cloud" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const bitbucketCloudEventRouterEventsModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-backend-module-bitbucket-cloud/api-report.md b/plugins/events-backend-module-bitbucket-cloud/api-report.md index c47252ce17..4795edd89a 100644 --- a/plugins/events-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/events-backend-module-bitbucket-cloud/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { EventParams } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -13,7 +12,4 @@ export class BitbucketCloudEventRouter extends SubTopicEventRouter { // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } - -// @alpha -export const bitbucketCloudEventRouterEventsModule: () => BackendFeature; ``` diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 7c6496c946..0b1c5d08a7 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,21 +1,33 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -34,7 +46,6 @@ "supertest": "^6.1.3" }, "files": [ - "alpha", "dist" ] } diff --git a/plugins/events-backend-module-bitbucket-cloud/src/alpha.ts b/plugins/events-backend-module-bitbucket-cloud/src/alpha.ts new file mode 100644 index 0000000000..e19ff9c9b5 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { bitbucketCloudEventRouterEventsModule } from './service/BitbucketCloudEventRouterEventsModule'; diff --git a/plugins/events-backend-module-bitbucket-cloud/src/index.ts b/plugins/events-backend-module-bitbucket-cloud/src/index.ts index 0a58212de2..77aa9798ef 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/index.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/index.ts @@ -22,4 +22,3 @@ */ export { BitbucketCloudEventRouter } from './router/BitbucketCloudEventRouter'; -export { bitbucketCloudEventRouterEventsModule } from './service/BitbucketCloudEventRouterEventsModule'; diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.test.ts index e456607a2b..60ce5b713e 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.test.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.test.ts @@ -15,7 +15,7 @@ */ import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { bitbucketCloudEventRouterEventsModule } from './BitbucketCloudEventRouterEventsModule'; import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.ts index 7f755a28f4..9516651aa3 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.ts @@ -15,13 +15,13 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; /** * Module for the events-backend plugin, adding an event router for Bitbucket Cloud. * - * Registers the {@link BitbucketCloudEventRouter}. + * Registers the `BitbucketCloudEventRouter`. * * @alpha */ diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index 8fdb5fd6d8..f74060b9bd 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/alpha-api-report.md b/plugins/events-backend-module-gerrit/alpha-api-report.md new file mode 100644 index 0000000000..f610d518e7 --- /dev/null +++ b/plugins/events-backend-module-gerrit/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-events-backend-module-gerrit" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const gerritEventRouterEventsModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-backend-module-gerrit/api-report.md b/plugins/events-backend-module-gerrit/api-report.md index c8d4a7c9b6..ba3c4dd29f 100644 --- a/plugins/events-backend-module-gerrit/api-report.md +++ b/plugins/events-backend-module-gerrit/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { EventParams } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -13,7 +12,4 @@ export class GerritEventRouter extends SubTopicEventRouter { // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } - -// @alpha -export const gerritEventRouterEventsModule: () => BackendFeature; ``` diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 98832ea84c..ec9f9358ab 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,21 +1,33 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -34,7 +46,6 @@ "supertest": "^6.1.3" }, "files": [ - "alpha", "dist" ] } diff --git a/plugins/events-backend-module-gerrit/src/alpha.ts b/plugins/events-backend-module-gerrit/src/alpha.ts new file mode 100644 index 0000000000..26fd89b14d --- /dev/null +++ b/plugins/events-backend-module-gerrit/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { gerritEventRouterEventsModule } from './service/GerritEventRouterEventsModule'; diff --git a/plugins/events-backend-module-gerrit/src/index.ts b/plugins/events-backend-module-gerrit/src/index.ts index 1a9ebb03ee..dfc314667f 100644 --- a/plugins/events-backend-module-gerrit/src/index.ts +++ b/plugins/events-backend-module-gerrit/src/index.ts @@ -22,4 +22,3 @@ */ export { GerritEventRouter } from './router/GerritEventRouter'; -export { gerritEventRouterEventsModule } from './service/GerritEventRouterEventsModule'; diff --git a/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.test.ts b/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.test.ts index b5fd6a80ca..6a8f4f46ff 100644 --- a/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.test.ts +++ b/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.test.ts @@ -15,7 +15,7 @@ */ import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { gerritEventRouterEventsModule } from './GerritEventRouterEventsModule'; import { GerritEventRouter } from '../router/GerritEventRouter'; diff --git a/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.ts b/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.ts index 734b574edd..31f5c9b384 100644 --- a/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.ts +++ b/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.ts @@ -15,13 +15,13 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { GerritEventRouter } from '../router/GerritEventRouter'; /** * Module for the events-backend plugin, adding an event router for Gerrit. * - * Registers the {@link GerritEventRouter}. + * Registers the `GerritEventRouter`. * * @alpha */ diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index 47fdc41032..ff885d1dbf 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-events-backend-module-github +## 0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/config@1.0.6 + +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/events-backend-module-github/alpha-api-report.md b/plugins/events-backend-module-github/alpha-api-report.md new file mode 100644 index 0000000000..cb95928b17 --- /dev/null +++ b/plugins/events-backend-module-github/alpha-api-report.md @@ -0,0 +1,15 @@ +## API Report File for "@backstage/plugin-events-backend-module-github" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const githubEventRouterEventsModule: () => BackendFeature; + +// @alpha +export const githubWebhookEventsModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-backend-module-github/api-report.md b/plugins/events-backend-module-github/api-report.md index 4f14bce789..bcf9b8ed74 100644 --- a/plugins/events-backend-module-github/api-report.md +++ b/plugins/events-backend-module-github/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; @@ -20,10 +19,4 @@ export class GithubEventRouter extends SubTopicEventRouter { // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } - -// @alpha -export const githubEventRouterEventsModule: () => BackendFeature; - -// @alpha -export const githubWebhookEventsModule: () => BackendFeature; ``` diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index 7d5fcf9981..ac7599b11f 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,21 +1,33 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -36,7 +48,6 @@ "supertest": "^6.1.3" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/events-backend-module-github/src/alpha.ts b/plugins/events-backend-module-github/src/alpha.ts new file mode 100644 index 0000000000..8a712cacb1 --- /dev/null +++ b/plugins/events-backend-module-github/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { githubEventRouterEventsModule } from './service/GithubEventRouterEventsModule'; +export { githubWebhookEventsModule } from './service/GithubWebhookEventsModule'; diff --git a/plugins/events-backend-module-github/src/index.ts b/plugins/events-backend-module-github/src/index.ts index 8e5671d9d8..d02ea1deaf 100644 --- a/plugins/events-backend-module-github/src/index.ts +++ b/plugins/events-backend-module-github/src/index.ts @@ -23,5 +23,3 @@ export { createGithubSignatureValidator } from './http/createGithubSignatureValidator'; export { GithubEventRouter } from './router/GithubEventRouter'; -export { githubEventRouterEventsModule } from './service/GithubEventRouterEventsModule'; -export { githubWebhookEventsModule } from './service/GithubWebhookEventsModule'; diff --git a/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.test.ts b/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.test.ts index 694f5e1dbe..0e2ca69366 100644 --- a/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.test.ts +++ b/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.test.ts @@ -15,7 +15,7 @@ */ import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { githubEventRouterEventsModule } from './GithubEventRouterEventsModule'; import { GithubEventRouter } from '../router/GithubEventRouter'; diff --git a/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.ts b/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.ts index 8914d07e71..56cc571763 100644 --- a/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.ts +++ b/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.ts @@ -15,13 +15,13 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { GithubEventRouter } from '../router/GithubEventRouter'; /** * Module for the events-backend plugin, adding an event router for GitHub. * - * Registers the {@link GithubEventRouter}. + * Registers the `GithubEventRouter`. * * @alpha */ diff --git a/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.test.ts b/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.test.ts index 84233eedee..9e33cac6a0 100644 --- a/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.test.ts +++ b/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.test.ts @@ -17,8 +17,8 @@ import { coreServices } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { - eventsExtensionPoint, HttpPostIngressOptions, RequestDetails, } from '@backstage/plugin-events-node'; diff --git a/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.ts b/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.ts index 9a3202cb97..10cb12e431 100644 --- a/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.ts +++ b/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.ts @@ -18,7 +18,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { createGithubSignatureValidator } from '../http/createGithubSignatureValidator'; /** diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index e87e9cf0ee..f3b2ac8594 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.1.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/plugin-events-node@0.2.3 + - @backstage/config@1.0.6 + +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/alpha-api-report.md b/plugins/events-backend-module-gitlab/alpha-api-report.md new file mode 100644 index 0000000000..64727cc844 --- /dev/null +++ b/plugins/events-backend-module-gitlab/alpha-api-report.md @@ -0,0 +1,15 @@ +## API Report File for "@backstage/plugin-events-backend-module-gitlab" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const gitlabEventRouterEventsModule: () => BackendFeature; + +// @alpha +export const gitlabWebhookEventsModule: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-backend-module-gitlab/api-report.md b/plugins/events-backend-module-gitlab/api-report.md index 2a88295032..8a0c513857 100644 --- a/plugins/events-backend-module-gitlab/api-report.md +++ b/plugins/events-backend-module-gitlab/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; @@ -18,10 +17,4 @@ export class GitlabEventRouter extends SubTopicEventRouter { // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } - -// @alpha -export const gitlabEventRouterEventsModule: () => BackendFeature; - -// @alpha -export const gitlabWebhookEventsModule: () => BackendFeature; ``` diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index 8c7d08ef71..09c924394b 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,21 +1,33 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin-module" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -35,7 +47,6 @@ "supertest": "^6.1.3" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/events-backend-module-gitlab/src/alpha.ts b/plugins/events-backend-module-gitlab/src/alpha.ts new file mode 100644 index 0000000000..357c9f81ce --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { gitlabEventRouterEventsModule } from './service/GitlabEventRouterEventsModule'; +export { gitlabWebhookEventsModule } from './service/GitlabWebhookEventsModule'; diff --git a/plugins/events-backend-module-gitlab/src/index.ts b/plugins/events-backend-module-gitlab/src/index.ts index 859a6c69c0..3788e02955 100644 --- a/plugins/events-backend-module-gitlab/src/index.ts +++ b/plugins/events-backend-module-gitlab/src/index.ts @@ -23,5 +23,3 @@ export { createGitlabTokenValidator } from './http/createGitlabTokenValidator'; export { GitlabEventRouter } from './router/GitlabEventRouter'; -export { gitlabEventRouterEventsModule } from './service/GitlabEventRouterEventsModule'; -export { gitlabWebhookEventsModule } from './service/GitlabWebhookEventsModule'; diff --git a/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.test.ts b/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.test.ts index f0ffca3397..f8b03bc0f3 100644 --- a/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.test.ts +++ b/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.test.ts @@ -15,7 +15,7 @@ */ import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { gitlabEventRouterEventsModule } from './GitlabEventRouterEventsModule'; import { GitlabEventRouter } from '../router/GitlabEventRouter'; diff --git a/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.ts b/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.ts index 9ab2e1263e..39ccd5cd60 100644 --- a/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.ts +++ b/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.ts @@ -15,13 +15,13 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { GitlabEventRouter } from '../router/GitlabEventRouter'; /** * Module for the events-backend plugin, adding an event router for GitLab. * - * Registers the {@link GitlabEventRouter}. + * Registers the `GitlabEventRouter`. * * @alpha */ diff --git a/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.test.ts b/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.test.ts index b139f46f55..b42f2c474e 100644 --- a/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.test.ts +++ b/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.test.ts @@ -17,8 +17,8 @@ import { coreServices } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { - eventsExtensionPoint, HttpPostIngressOptions, RequestDetails, } from '@backstage/plugin-events-node'; diff --git a/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.ts b/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.ts index 44f13f31ef..c20e55caa9 100644 --- a/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.ts +++ b/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.ts @@ -18,7 +18,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { createGitlabTokenValidator } from '../http/createGitlabTokenValidator'; /** @@ -26,7 +26,7 @@ import { createGitlabTokenValidator } from '../http/createGitlabTokenValidator'; * registering an HTTP POST ingress with request validator * which verifies the webhook token based on a secret. * - * Registers the {@link GitlabEventRouter}. + * Registers the `GitlabEventRouter`. * * @alpha */ diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index ad60c22bf1..2394b67a48 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.4-next.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3 + +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3-next.2 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index 26a8f9bdb4..2a4f45aa73 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-events-backend-test-utils", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index 7fe73314f5..0e04c52691 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/plugin-events-backend +## 0.2.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/plugin-events-node@0.2.4-next.0 + - @backstage/config@1.0.6 + +## 0.2.3 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/plugin-events-node@0.2.3 + - @backstage/config@1.0.6 + +## 0.2.3-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-events-node@0.2.3-next.1 + +## 0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + - @backstage/plugin-events-node@0.2.3-next.0 + ## 0.2.1 ### Patch Changes diff --git a/plugins/events-backend/alpha-api-report.md b/plugins/events-backend/alpha-api-report.md new file mode 100644 index 0000000000..3e32f769c1 --- /dev/null +++ b/plugins/events-backend/alpha-api-report.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-events-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +export const eventsPlugin: () => BackendFeature; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md index 28437772ae..7d8ede1979 100644 --- a/plugins/events-backend/api-report.md +++ b/plugins/events-backend/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; @@ -28,9 +27,6 @@ export class EventsBackend { start(): Promise<void>; } -// @alpha -export const eventsPlugin: () => BackendFeature; - // @public export class HttpPostIngressEventPublisher implements EventPublisher { // (undocumented) diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index 23e388e2b9..fa3594528c 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,21 +1,33 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.2.1", + "version": "0.2.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -40,7 +52,6 @@ "supertest": "^6.1.3" }, "files": [ - "alpha", "config.d.ts", "dist" ], diff --git a/plugins/events-backend/src/alpha.ts b/plugins/events-backend/src/alpha.ts new file mode 100644 index 0000000000..4403da1636 --- /dev/null +++ b/plugins/events-backend/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { eventsPlugin } from './service/EventsPlugin'; diff --git a/plugins/events-backend/src/index.ts b/plugins/events-backend/src/index.ts index 5026729015..4645e9a479 100644 --- a/plugins/events-backend/src/index.ts +++ b/plugins/events-backend/src/index.ts @@ -21,5 +21,4 @@ */ export { EventsBackend } from './service/EventsBackend'; -export { eventsPlugin } from './service/EventsPlugin'; export { HttpPostIngressEventPublisher } from './service/http'; diff --git a/plugins/events-backend/src/service/EventsPlugin.test.ts b/plugins/events-backend/src/service/EventsPlugin.test.ts index 0570b16d01..73578ae222 100644 --- a/plugins/events-backend/src/service/EventsPlugin.test.ts +++ b/plugins/events-backend/src/service/EventsPlugin.test.ts @@ -21,7 +21,7 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { TestEventBroker, TestEventPublisher, diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index 29456d71f7..1ae038a2ac 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -19,12 +19,14 @@ import { coreServices, } from '@backstage/backend-plugin-api'; import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + eventsExtensionPoint, + EventsExtensionPoint, +} from '@backstage/plugin-events-node/alpha'; import { EventBroker, EventPublisher, EventSubscriber, - eventsExtensionPoint, - EventsExtensionPoint, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; import { InMemoryEventBroker } from './InMemoryEventBroker'; @@ -80,7 +82,7 @@ class EventsExtensionPointImpl implements EventsExtensionPoint { * @alpha */ export const eventsPlugin = createBackendPlugin({ - id: 'events', + pluginId: 'events', register(env) { const extensionPoint = new EventsExtensionPointImpl(); env.registerExtensionPoint(eventsExtensionPoint, extensionPoint); diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index e8f37ea791..3c999d18d6 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-events-node +## 0.2.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + +## 0.2.3 + +### Patch Changes + +- 19d4abf72c: Make `EventParams` typed for implementing tidier event handling. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + +## 0.2.3-next.2 + +### Patch Changes + +- 19d4abf72c: Make `EventParams` typed for implementing tidier event handling. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + +## 0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 0.2.1 ### Patch Changes diff --git a/plugins/events-node/alpha-api-report.md b/plugins/events-node/alpha-api-report.md new file mode 100644 index 0000000000..fd30f54d45 --- /dev/null +++ b/plugins/events-node/alpha-api-report.md @@ -0,0 +1,32 @@ +## API Report File for "@backstage/plugin-events-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { EventBroker } from '@backstage/plugin-events-node'; +import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventSubscriber } from '@backstage/plugin-events-node'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; + +// @alpha (undocumented) +export interface EventsExtensionPoint { + // (undocumented) + addHttpPostIngress(options: HttpPostIngressOptions): void; + // (undocumented) + addPublishers( + ...publishers: Array<EventPublisher | Array<EventPublisher>> + ): void; + // (undocumented) + addSubscribers( + ...subscribers: Array<EventSubscriber | Array<EventSubscriber>> + ): void; + // (undocumented) + setEventBroker(eventBroker: EventBroker): void; +} + +// @alpha (undocumented) +export const eventsExtensionPoint: ExtensionPoint<EventsExtensionPoint>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index 51136fd301..dfda48d97e 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { ExtensionPoint } from '@backstage/backend-plugin-api'; - // @public export interface EventBroker { publish(params: EventParams): Promise<void>; @@ -14,8 +12,8 @@ export interface EventBroker { } // @public (undocumented) -export interface EventParams { - eventPayload: unknown; +export interface EventParams<TPayload = unknown> { + eventPayload: TPayload; metadata?: Record<string, string | string[] | undefined>; topic: string; } @@ -40,25 +38,6 @@ export abstract class EventRouter implements EventPublisher, EventSubscriber { abstract supportsEventTopics(): string[]; } -// @alpha (undocumented) -export interface EventsExtensionPoint { - // (undocumented) - addHttpPostIngress(options: HttpPostIngressOptions): void; - // (undocumented) - addPublishers( - ...publishers: Array<EventPublisher | Array<EventPublisher>> - ): void; - // (undocumented) - addSubscribers( - ...subscribers: Array<EventSubscriber | Array<EventSubscriber>> - ): void; - // (undocumented) - setEventBroker(eventBroker: EventBroker): void; -} - -// @alpha (undocumented) -export const eventsExtensionPoint: ExtensionPoint<EventsExtensionPoint>; - // @public export interface EventSubscriber { onEvent(params: EventParams): Promise<void>; diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 7d7992db7c..5d6327980e 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,22 +1,34 @@ { "name": "@backstage/plugin-events-node", "description": "The plugin-events-node module for @backstage/plugin-events-backend", - "version": "0.2.1", + "version": "0.2.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "node-library" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", @@ -30,7 +42,6 @@ "@backstage/cli": "workspace:^" }, "files": [ - "alpha", "dist" ] } diff --git a/plugins/events-node/src/alpha.ts b/plugins/events-node/src/alpha.ts new file mode 100644 index 0000000000..90666ba2f3 --- /dev/null +++ b/plugins/events-node/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { EventsExtensionPoint } from './extensions'; +export { eventsExtensionPoint } from './extensions'; diff --git a/plugins/events-node/src/api/EventParams.ts b/plugins/events-node/src/api/EventParams.ts index 6912827281..19a562aee0 100644 --- a/plugins/events-node/src/api/EventParams.ts +++ b/plugins/events-node/src/api/EventParams.ts @@ -17,7 +17,7 @@ /** * @public */ -export interface EventParams { +export interface EventParams<TPayload = unknown> { /** * Topic for which this event should be published. */ @@ -25,7 +25,7 @@ export interface EventParams { /** * Event payload. */ - eventPayload: unknown; + eventPayload: TPayload; /** * Metadata (e.g., HTTP headers and similar for events received from external). */ diff --git a/plugins/events-node/src/extensions.ts b/plugins/events-node/src/extensions.ts index 945d86b49c..2e44b381af 100644 --- a/plugins/events-node/src/extensions.ts +++ b/plugins/events-node/src/extensions.ts @@ -20,7 +20,7 @@ import { EventPublisher, EventSubscriber, HttpPostIngressOptions, -} from './api'; +} from '@backstage/plugin-events-node'; /** * @alpha diff --git a/plugins/events-node/src/index.ts b/plugins/events-node/src/index.ts index 422eeb169e..2bdf456f13 100644 --- a/plugins/events-node/src/index.ts +++ b/plugins/events-node/src/index.ts @@ -21,5 +21,3 @@ */ export * from './api'; -export type { EventsExtensionPoint } from './extensions'; -export { eventsExtensionPoint } from './extensions'; diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index cf6ea72eb9..e0356edf92 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,56 @@ # @internal/plugin-todo-list-backend +## 1.0.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + +## 1.0.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + +## 1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 1.0.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## 1.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + ## 1.0.9 ### Patch Changes diff --git a/plugins/example-todo-list-backend/api-report.md b/plugins/example-todo-list-backend/api-report.md index c4f8665a70..319fbbf226 100644 --- a/plugins/example-todo-list-backend/api-report.md +++ b/plugins/example-todo-list-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 { BackendFeature } from '@backstage/backend-plugin-api'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; @@ -10,6 +11,9 @@ import { Logger } from 'winston'; // @public export function createRouter(options: RouterOptions): Promise<express.Router>; +// @alpha +export const exampleTodoListPlugin: () => BackendFeature; + // @public export interface RouterOptions { // (undocumented) diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index bf8eaa48dd..963bf847ac 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.9", + "version": "1.0.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,11 +11,12 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -24,6 +25,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", @@ -38,10 +40,11 @@ "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.6" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/example-todo-list-backend/src/index.ts b/plugins/example-todo-list-backend/src/index.ts index ca73cb27ba..6917307db6 100644 --- a/plugins/example-todo-list-backend/src/index.ts +++ b/plugins/example-todo-list-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export { exampleTodoListPlugin } from './plugin'; diff --git a/plugins/example-todo-list-backend/src/plugin.ts b/plugins/example-todo-list-backend/src/plugin.ts new file mode 100644 index 0000000000..42985f6e88 --- /dev/null +++ b/plugins/example-todo-list-backend/src/plugin.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The example TODO list backend plugin. + * + * @alpha + */ +export const exampleTodoListPlugin = createBackendPlugin({ + pluginId: 'exampleTodoList', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ identity, logger, httpRouter }) { + httpRouter.use( + await createRouter({ + identity, + logger: loggerToWinstonLogger(logger), + }), + ); + }, + }); + }, +}); diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index c1be8da2f8..3d7a66e012 100644 --- a/plugins/example-todo-list-common/package.json +++ b/plugins/example-todo-list-common/package.json @@ -32,7 +32,7 @@ "@backstage/test-utils": "workspace:^", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/example-todo-list/.eslintrc.js b/plugins/example-todo-list/.eslintrc.js index 8c18e76229..e2a53a6ad2 100644 --- a/plugins/example-todo-list/.eslintrc.js +++ b/plugins/example-todo-list/.eslintrc.js @@ -1,14 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], - rules: { - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], - }, -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index 66c920d08b..715a9bfc12 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,41 @@ # @internal/plugin-todo-list +## 1.0.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 1.0.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## 1.0.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 1.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 1.0.9 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index ce8197bc6e..4eb7d3357f 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.9", + "version": "1.0.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/explore-backend/CHANGELOG.md b/plugins/explore-backend/CHANGELOG.md index 39507030de..42a632ec7f 100644 --- a/plugins/explore-backend/CHANGELOG.md +++ b/plugins/explore-backend/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-explore-backend +## 0.0.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + +## 0.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + +## 0.0.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + +## 0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + +## 0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.0.2 ### Patch Changes diff --git a/plugins/explore-backend/README.md b/plugins/explore-backend/README.md index 3edf48249a..86f60ae226 100644 --- a/plugins/explore-backend/README.md +++ b/plugins/explore-backend/README.md @@ -76,10 +76,9 @@ To index explore tools you will need to register the search collator in the ```diff +import { ToolDocumentCollatorFactory } from '@backstage/plugin-explore-backend'; -async function createSearchEngine( - env: PluginEnvironment, -): Promise<SearchEngine> { - ... +... + ++ // collator gathers entities from explore. + indexBuilder.addCollator({ + schedule, + factory: ToolDocumentCollatorFactory.fromConfig(env.config, { @@ -87,8 +86,8 @@ async function createSearchEngine( + logger: env.logger, + }), + }); - ... -} + +... ``` ### Wire up the Frontend diff --git a/plugins/explore-backend/package.json b/plugins/explore-backend/package.json index eca8761c84..0a6de1ab85 100644 --- a/plugins/explore-backend/package.json +++ b/plugins/explore-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore-backend", - "version": "0.0.2", + "version": "0.0.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,7 +37,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.2.4" }, "files": [ diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md index 2dc2181597..cb3adf2f5d 100644 --- a/plugins/explore-react/CHANGELOG.md +++ b/plugins/explore-react/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-explore-react +## 0.0.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/plugin-explore-common@0.0.1 + +## 0.0.26 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-explore-common@0.0.1 + ## 0.0.25 ### Patch Changes diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index e5a47a4ab7..2aa8f1b6fc 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.25", + "version": "0.0.27-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -44,7 +44,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 4730211b0d..501f4dd10c 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,95 @@ # @backstage/plugin-explore +## 0.4.1-next.0 + +### Patch Changes + +- 3f75b7607c: Add ability to pass icon as function to have ability to customize it by search item +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.27-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## 0.4.0 + +### Minor Changes + +- 0eaa579f89: The `ToolSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.26 + - @backstage/plugin-search-common@1.2.1 + +## 0.4.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.25 + - @backstage/plugin-search-common@1.2.1 + +## 0.4.0-next.1 + +### Minor Changes + +- 0eaa579f89: The `ToolSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.25 + - @backstage/plugin-search-common@1.2.1 + +## 0.3.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-explore-react@0.0.25 + ## 0.3.45 ### Patch Changes diff --git a/plugins/explore/README.md b/plugins/explore/README.md index 2388d97ac1..0bb7258ae6 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -30,7 +30,7 @@ any path for the route, but we recommend the following: ```diff // packages/app/src/App.tsx -+ import { ExplorePage, explorePlugin } from '@backstage/plugin-explore'; ++ import { ExplorePage } from '@backstage/plugin-explore'; ... diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index d184ce01a4..8820411392 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -19,6 +19,7 @@ import { IndexableDocument } from '@backstage/plugin-search-common'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; +import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react'; import { TabProps } from '@material-ui/core'; // @public @deprecated (undocumented) @@ -125,19 +126,19 @@ export const ToolExplorerContent: (props: { }) => JSX.Element; // @public (undocumented) -export function ToolSearchResultListItem( - props: ToolSearchResultListItemProps, -): JSX.Element; +export const ToolSearchResultListItem: ( + props: SearchResultListItemExtensionProps<ToolSearchResultListItemProps>, +) => JSX.Element | null; // @public export interface ToolSearchResultListItemProps { // (undocumented) highlight?: ResultHighlight; // (undocumented) - icon?: ReactNode; + icon?: ReactNode | ((result: IndexableDocument) => ReactNode); // (undocumented) rank?: number; // (undocumented) - result: IndexableDocument; + result?: IndexableDocument; } ``` diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 19ec095c09..c53f00ff6a 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.45", + "version": "0.4.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -64,7 +64,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx index a20a5e1f0c..0d37e3e273 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -31,7 +31,7 @@ import { } from '@material-ui/core'; import React from 'react'; -import { Button, ItemCardHeader } from '@backstage/core-components'; +import { LinkButton, ItemCardHeader } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; /** @public */ @@ -66,9 +66,9 @@ export const DomainCard = (props: { entity: DomainEntity }) => { {entity.metadata.description} </CardContent> <CardActions> - <Button to={url} color="primary"> + <LinkButton to={url} color="primary"> Explore - </Button> + </LinkButton> </CardActions> </Card> ); diff --git a/plugins/explore/src/components/ToolCard/ToolCard.tsx b/plugins/explore/src/components/ToolCard/ToolCard.tsx index 904db0c00b..27729ea085 100644 --- a/plugins/explore/src/components/ToolCard/ToolCard.tsx +++ b/plugins/explore/src/components/ToolCard/ToolCard.tsx @@ -26,7 +26,7 @@ import { makeStyles, Typography, } from '@material-ui/core'; -import { Button } from '@backstage/core-components'; +import { LinkButton } from '@backstage/core-components'; import classNames from 'classnames'; import React from 'react'; @@ -97,9 +97,9 @@ export const ToolCard = ({ card, objectFit }: Props) => { )} </CardContent> <CardActions> - <Button color="primary" to={url} disabled={!url}> + <LinkButton color="primary" to={url} disabled={!url}> Explore - </Button> + </LinkButton> </CardActions> </Card> ); diff --git a/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx b/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx index 3f5a515efb..1aad122ee6 100644 --- a/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx +++ b/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx @@ -18,14 +18,11 @@ import React, { ReactNode } from 'react'; import { Box, Chip, - Divider, - ListItem, ListItemIcon, ListItemText, makeStyles, } from '@material-ui/core'; import { Link } from '@backstage/core-components'; -import { useAnalytics } from '@backstage/core-plugin-api'; import { IndexableDocument, ResultHighlight, @@ -49,67 +46,62 @@ const useStyles = makeStyles({ * @public */ export interface ToolSearchResultListItemProps { - icon?: ReactNode; - result: IndexableDocument; + icon?: ReactNode | ((result: IndexableDocument) => ReactNode); + result?: IndexableDocument; highlight?: ResultHighlight; rank?: number; } -/** @public */ +/** @public */ export function ToolSearchResultListItem(props: ToolSearchResultListItemProps) { const result = props.result as any; const classes = useStyles(); - const analytics = useAnalytics(); - const handleClick = () => { - analytics.captureEvent('discover', result.title, { - attributes: { to: result.location }, - value: props.rank, - }); - }; + + if (!result) return null; return ( <> - <ListItem alignItems="flex-start"> - {props.icon && <ListItemIcon>{props.icon}</ListItemIcon>} - <div className={classes.flexContainer}> - <ListItemText - className={classes.itemText} - primaryTypographyProps={{ variant: 'h6' }} - primary={ - <Link noTrack to={result.location} onClick={handleClick}> - {props.highlight?.fields.title ? ( - <HighlightedSearchResultText - text={props.highlight.fields.title} - preTag={props.highlight.preTag} - postTag={props.highlight.postTag} - /> - ) : ( - result.title - )} - </Link> - } - secondary={ - props.highlight?.fields.text ? ( + {props.icon && ( + <ListItemIcon> + {typeof props.icon === 'function' ? props.icon(result) : props.icon} + </ListItemIcon> + )} + <div className={classes.flexContainer}> + <ListItemText + className={classes.itemText} + primaryTypographyProps={{ variant: 'h6' }} + primary={ + <Link noTrack to={result.location}> + {props.highlight?.fields.title ? ( <HighlightedSearchResultText - text={props.highlight.fields.text} + text={props.highlight.fields.title} preTag={props.highlight.preTag} postTag={props.highlight.postTag} /> ) : ( - result.text - ) - } - /> - <Box> - {result.tags && - result.tags.map((tag: string) => ( - <Chip label={tag} size="small" /> - ))} - </Box> - </div> - </ListItem> - <Divider component="li" /> + result.title + )} + </Link> + } + secondary={ + props.highlight?.fields.text ? ( + <HighlightedSearchResultText + text={props.highlight.fields.text} + preTag={props.highlight.preTag} + postTag={props.highlight.postTag} + /> + ) : ( + result.text + ) + } + /> + <Box> + {result.tags?.map((tag: string) => ( + <Chip label={tag} size="small" /> + ))} + </Box> + </div> </> ); } diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts index d3af1b99c2..f8fba16910 100644 --- a/plugins/explore/src/components/index.ts +++ b/plugins/explore/src/components/index.ts @@ -16,4 +16,3 @@ export * from './DomainCard'; export * from './ExploreLayout'; -export * from './ToolSearchResultListItem'; diff --git a/plugins/explore/src/index.ts b/plugins/explore/src/index.ts index 24591211f7..ec910640cd 100644 --- a/plugins/explore/src/index.ts +++ b/plugins/explore/src/index.ts @@ -23,5 +23,11 @@ export * from './api'; export * from './components'; export * from './extensions'; -export { explorePlugin, explorePlugin as plugin } from './plugin'; +export { + ToolSearchResultListItem, + explorePlugin, + explorePlugin as plugin, +} from './plugin'; export * from './routes'; + +export type { ToolSearchResultListItemProps } from './components/ToolSearchResultListItem'; diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts index ab8eedf7ef..84e7657355 100644 --- a/plugins/explore/src/plugin.ts +++ b/plugins/explore/src/plugin.ts @@ -22,7 +22,12 @@ import { discoveryApiRef, fetchApiRef, } from '@backstage/core-plugin-api'; +import { + createSearchResultListItemExtension, + SearchResultListItemExtensionProps, +} from '@backstage/plugin-search-react'; import { ExploreClient, exploreApiRef } from './api'; +import { ToolSearchResultListItemProps } from './components/ToolSearchResultListItem'; // import { exampleTools } from './util/examples'; /** @public */ @@ -65,3 +70,17 @@ export const explorePlugin = createPlugin({ catalogEntity: catalogEntityRouteRef, }, }); + +/** @public */ +export const ToolSearchResultListItem: ( + props: SearchResultListItemExtensionProps<ToolSearchResultListItemProps>, +) => JSX.Element | null = explorePlugin.provide( + createSearchResultListItemExtension({ + name: 'ToolSearchResultListItem', + component: () => + import('./components/ToolSearchResultListItem').then( + m => m.ToolSearchResultListItem, + ), + predicate: result => result.type === 'tools', + }), +); diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index f4156731bb..a83b8aa486 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-firehydrant +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + ## 0.1.31 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 6ddfa301b9..06edc1cd38 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.31", + "version": "0.1.33-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -46,7 +46,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index 3fc6dd9738..d7d62cc9fa 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-fossa +## 0.2.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.2.47 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## 0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.2.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.2.46 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 2ad62b8a4f..5864780dca 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.46", + "version": "0.2.48-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -59,7 +59,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/gcalendar/CHANGELOG.md b/plugins/gcalendar/CHANGELOG.md index 8155123f67..3878c11515 100644 --- a/plugins/gcalendar/CHANGELOG.md +++ b/plugins/gcalendar/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-gcalendar +## 0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/errors@1.1.4 + +## 0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + ## 0.3.10 ### Patch Changes diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json index dd6c804f98..37df64f7f4 100644 --- a/plugins/gcalendar/package.json +++ b/plugins/gcalendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcalendar", - "version": "0.3.10", + "version": "0.3.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -54,7 +54,7 @@ "@types/node": "^16.11.26", "@types/sanitize-html": "^2.6.2", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index 67ac992174..8b625586f9 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-gcp-projects +## 0.3.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.3.34 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## 0.3.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.3.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.3.33 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 193ab0af28..2a382f97bb 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.33", + "version": "0.3.35-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -55,7 +55,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 6d59cae145..5180d95013 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-git-release-manager +## 0.3.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## 0.3.28 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/integration@1.4.2 + +## 0.3.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## 0.3.28-next.0 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + ## 0.3.27 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index d3273e686d..63ebb36dfb 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.27", + "version": "0.3.29-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -53,7 +53,7 @@ "@types/node": "^16.11.26", "@types/recharts": "^1.8.15", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/git-release-manager/src/helpers/getShortCommitHash.ts b/plugins/git-release-manager/src/helpers/getShortCommitHash.ts index fd53ac7941..3b7a9cc310 100644 --- a/plugins/git-release-manager/src/helpers/getShortCommitHash.ts +++ b/plugins/git-release-manager/src/helpers/getShortCommitHash.ts @@ -17,7 +17,7 @@ import { GitReleaseManagerError } from '../errors/GitReleaseManagerError'; export function getShortCommitHash(hash: string) { - const shortCommitHash = hash.substr(0, 7); + const shortCommitHash = hash.slice(0, 7); if (shortCommitHash.length < 7) { throw new GitReleaseManagerError( diff --git a/plugins/git-release-manager/src/helpers/tagParts/getTagParts.ts b/plugins/git-release-manager/src/helpers/tagParts/getTagParts.ts index ba8412b2b4..3c500eb923 100644 --- a/plugins/git-release-manager/src/helpers/tagParts/getTagParts.ts +++ b/plugins/git-release-manager/src/helpers/tagParts/getTagParts.ts @@ -19,7 +19,7 @@ import { getSemverTagParts } from './getSemverTagParts'; import { Project } from '../../contexts/ProjectContext'; /** - * Tag parts are the individual parts of a version, e.g. <major>.<minor>.<patch> + * Tag parts are the individual parts of a version, e.g. [major].[minor].[patch] * are the parts of a semantic version * * @public diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 88cf0f6808..03eaff830e 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-github-actions +## 0.5.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## 0.5.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/integration@1.4.2 + +## 0.5.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.5.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## 0.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.5.14 ### Patch Changes diff --git a/plugins/github-actions/dev/index.tsx b/plugins/github-actions/dev/index.tsx index ee761b3504..93e6ee4f74 100644 --- a/plugins/github-actions/dev/index.tsx +++ b/plugins/github-actions/dev/index.tsx @@ -22,6 +22,8 @@ import { Entity } from '@backstage/catalog-model'; import { GithubActionsApi, githubActionsApiRef } from '../src'; import getWorkflowRunResponse from '../src/__fixtures__/get-workflow-run.json'; import listJobsForWorkflowRunResponse from '../src/__fixtures__/list-jobs-for-workflow-run.json'; +import listWorkflowRuns from '../src/__fixtures__/list-workflow-runs.json'; +import { downloadJobLogsForWorkflowRun } from '../src/__fixtures__/downloadJobLogsForWorkflowRun'; const mockEntity: Entity = { apiVersion: 'backstage.io/v1alpha1', @@ -41,7 +43,9 @@ const mockEntity: Entity = { }; const mockGithubActionsApi: GithubActionsApi = { - async downloadJobLogsForWorkflowRun() {}, + async downloadJobLogsForWorkflowRun() { + return downloadJobLogsForWorkflowRun; + }, async getWorkflow() { return {} as any; }, @@ -52,7 +56,7 @@ const mockGithubActionsApi: GithubActionsApi = { return listJobsForWorkflowRunResponse as any; }, async listWorkflowRuns() { - return {} as any; + return listWorkflowRuns as any; }, async reRunWorkflow() { return {} as any; diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 93320fe766..8a51def8c4 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.5.14", + "version": "0.5.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -61,7 +61,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/github-actions/src/__fixtures__/downloadJobLogsForWorkflowRun.ts b/plugins/github-actions/src/__fixtures__/downloadJobLogsForWorkflowRun.ts new file mode 100644 index 0000000000..7b882d0719 --- /dev/null +++ b/plugins/github-actions/src/__fixtures__/downloadJobLogsForWorkflowRun.ts @@ -0,0 +1,315 @@ +/* + * 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 const downloadJobLogsForWorkflowRun = ` +2022-12-09T17:25:25.6542422Z Requested labels: ubuntu-latest +2022-12-09T17:25:25.6542470Z Job defined at: octo-org/octo-template/dynamic/pages/pages-build-deployment@refs/heads/main +2022-12-09T17:25:25.6542493Z Waiting for a runner to pick up this job... +2022-12-09T17:25:25.9157301Z Job is waiting for a hosted runner to come online. +2022-12-09T17:25:29.3998345Z Job is about to start running on the hosted runner: Hosted Agent (hosted) +2022-12-09T17:25:31.7099053Z Current runner version: '2.299.1' +2022-12-09T17:25:31.7123983Z ##[group]Operating System +2022-12-09T17:25:31.7124496Z Ubuntu +2022-12-09T17:25:31.7124859Z 22.04.1 +2022-12-09T17:25:31.7125085Z LTS +2022-12-09T17:25:31.7125353Z ##[endgroup] +2022-12-09T17:25:31.7125657Z ##[group]Runner Image +2022-12-09T17:25:31.7125963Z Image: ubuntu-22.04 +2022-12-09T17:25:31.7126295Z Version: 20221204.2 +2022-12-09T17:25:31.7127048Z Included Software: https://github.com/actions/runner-images/blob/ubuntu22/20221204.2/images/linux/Ubuntu2204-Readme.md +2022-12-09T17:25:31.7127614Z Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu22%2F20221204.2 +2022-12-09T17:25:31.7128026Z ##[endgroup] +2022-12-09T17:25:31.7128400Z ##[group]Runner Image Provisioner +2022-12-09T17:25:31.7128722Z 2.0.91.1 +2022-12-09T17:25:31.7128945Z ##[endgroup] +2022-12-09T17:25:31.7129586Z ##[group]GITHUB_TOKEN Permissions +2022-12-09T17:25:31.7130130Z Contents: read +2022-12-09T17:25:31.7130387Z Metadata: read +2022-12-09T17:25:31.7130849Z Pages: write +2022-12-09T17:25:31.7131169Z ##[endgroup] +2022-12-09T17:25:31.7134756Z Secret source: Actions +2022-12-09T17:25:31.7135232Z Prepare workflow directory +2022-12-09T17:25:31.7951647Z Prepare all required actions +2022-12-09T17:25:31.8134091Z Getting action download info +2022-12-09T17:25:32.1190857Z Download action repository 'actions/checkout@v3' (SHA:93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8) +2022-12-09T17:25:32.4403240Z Download action repository 'actions/jekyll-build-pages@v1' (SHA:9b882383c88a22eef81e9c1a5a232c2ff9a6672b) +2022-12-09T17:25:32.7457755Z Download action repository 'actions/upload-pages-artifact@v1' (SHA:c8641e80048cf33d329c39df3bcc53196178335a) +2022-12-09T17:25:33.0262452Z Getting action download info +2022-12-09T17:25:33.1786870Z Download action repository 'actions/upload-artifact@main' (SHA:83fd05a356d7e2593de66fc9913b3002723633cb) +2022-12-09T17:25:33.4537715Z ##[group]Pull down action image 'ghcr.io/actions/jekyll-build-pages:v1.0.4' +2022-12-09T17:25:33.4611360Z ##[command]/usr/bin/docker pull ghcr.io/actions/jekyll-build-pages:v1.0.4 +2022-12-09T17:25:34.0231935Z v1.0.4: Pulling from actions/jekyll-build-pages +2022-12-09T17:25:34.0232298Z eff15d958d66: Pulling fs layer +2022-12-09T17:25:34.0232609Z 923e91ae3a1b: Pulling fs layer +2022-12-09T17:25:34.0232986Z 2aa5d3a4a151: Pulling fs layer +2022-12-09T17:25:34.0233248Z bc64adf2d0b2: Pulling fs layer +2022-12-09T17:25:34.0233543Z bfc5cca7d80e: Pulling fs layer +2022-12-09T17:25:34.0233827Z 61b6e3e25463: Pulling fs layer +2022-12-09T17:25:34.0234080Z 7c62fb63f605: Pulling fs layer +2022-12-09T17:25:34.0234380Z 0f68cc81f8f6: Pulling fs layer +2022-12-09T17:25:34.0234663Z 09f325326117: Pulling fs layer +2022-12-09T17:25:34.0234894Z a73a0a50c4db: Pulling fs layer +2022-12-09T17:25:34.0235171Z bc64adf2d0b2: Waiting +2022-12-09T17:25:34.0235470Z bfc5cca7d80e: Waiting +2022-12-09T17:25:34.0235840Z 61b6e3e25463: Waiting +2022-12-09T17:25:34.0236104Z 7c62fb63f605: Waiting +2022-12-09T17:25:34.0236314Z 0f68cc81f8f6: Waiting +2022-12-09T17:25:34.0236586Z 09f325326117: Waiting +2022-12-09T17:25:34.0236855Z a73a0a50c4db: Waiting +2022-12-09T17:25:34.1800756Z 2aa5d3a4a151: Verifying Checksum +2022-12-09T17:25:34.1801065Z 2aa5d3a4a151: Download complete +2022-12-09T17:25:34.4333121Z eff15d958d66: Verifying Checksum +2022-12-09T17:25:34.4335284Z eff15d958d66: Download complete +2022-12-09T17:25:34.5215161Z bc64adf2d0b2: Verifying Checksum +2022-12-09T17:25:34.5231169Z bc64adf2d0b2: Download complete +2022-12-09T17:25:34.5266358Z 923e91ae3a1b: Verifying Checksum +2022-12-09T17:25:34.5266637Z 923e91ae3a1b: Download complete +2022-12-09T17:25:34.6185999Z bfc5cca7d80e: Verifying Checksum +2022-12-09T17:25:34.6186341Z bfc5cca7d80e: Download complete +2022-12-09T17:25:34.6627434Z 7c62fb63f605: Verifying Checksum +2022-12-09T17:25:34.6629228Z 7c62fb63f605: Download complete +2022-12-09T17:25:34.8423112Z 09f325326117: Verifying Checksum +2022-12-09T17:25:34.8424383Z 09f325326117: Download complete +2022-12-09T17:25:35.0142784Z a73a0a50c4db: Verifying Checksum +2022-12-09T17:25:35.0143743Z a73a0a50c4db: Download complete +2022-12-09T17:25:35.2282893Z 0f68cc81f8f6: Verifying Checksum +2022-12-09T17:25:35.2283672Z 0f68cc81f8f6: Download complete +2022-12-09T17:25:35.8547685Z 61b6e3e25463: Verifying Checksum +2022-12-09T17:25:35.8547985Z 61b6e3e25463: Download complete +2022-12-09T17:25:36.3689873Z eff15d958d66: Pull complete +2022-12-09T17:25:38.9751393Z 923e91ae3a1b: Pull complete +2022-12-09T17:25:39.0574850Z 2aa5d3a4a151: Pull complete +2022-12-09T17:25:39.6785807Z bc64adf2d0b2: Pull complete +2022-12-09T17:25:39.7560781Z bfc5cca7d80e: Pull complete +2022-12-09T17:25:45.3145640Z 61b6e3e25463: Pull complete +2022-12-09T17:25:45.3626474Z 7c62fb63f605: Pull complete +2022-12-09T17:25:48.5182928Z 0f68cc81f8f6: Pull complete +2022-12-09T17:25:48.5924407Z 09f325326117: Pull complete +2022-12-09T17:25:48.6361139Z a73a0a50c4db: Pull complete +2022-12-09T17:25:48.6390662Z Digest: sha256:e56d7e4c5ac45aa2934b1d535f4c66769a442ef6538c7948f8b4e3b2950fd7b4 +2022-12-09T17:25:48.6435854Z Status: Downloaded newer image for ghcr.io/actions/jekyll-build-pages:v1.0.4 +2022-12-09T17:25:48.6436263Z ghcr.io/actions/jekyll-build-pages:v1.0.4 +2022-12-09T17:25:48.6444026Z ##[endgroup] +2022-12-09T17:25:48.6814585Z ##[group]Run actions/checkout@v3 +2022-12-09T17:25:48.6814842Z with: +2022-12-09T17:25:48.6815018Z ref: main +2022-12-09T17:25:48.6815204Z submodules: recursive +2022-12-09T17:25:48.6815442Z repository: octo-org/octo-template +2022-12-09T17:25:48.6815884Z token: *** +2022-12-09T17:25:48.6816068Z ssh-strict: true +2022-12-09T17:25:48.6816284Z persist-credentials: true +2022-12-09T17:25:48.6816500Z clean: true +2022-12-09T17:25:48.6816685Z fetch-depth: 1 +2022-12-09T17:25:48.6816872Z lfs: false +2022-12-09T17:25:48.6817076Z set-safe-directory: true +2022-12-09T17:25:48.6817275Z ##[endgroup] +2022-12-09T17:25:48.9194010Z Syncing repository: octo-org/octo-template +2022-12-09T17:25:48.9195798Z ##[group]Getting Git version info +2022-12-09T17:25:48.9196263Z Working directory is '/home/runner/work/octo-template/octo-template' +2022-12-09T17:25:48.9196777Z [command]/usr/bin/git version +2022-12-09T17:25:48.9267188Z git version 2.38.1 +2022-12-09T17:25:48.9293570Z ##[endgroup] +2022-12-09T17:25:48.9315385Z Temporarily overriding HOME='/home/runner/work/_temp/54730a3a-82b6-4069-a509-366e53e131cc' before making global git config changes +2022-12-09T17:25:48.9315845Z Adding repository directory to the temporary git global config as a safe directory +2022-12-09T17:25:48.9316365Z [command]/usr/bin/git config --global --add safe.directory /home/runner/work/octo-template/octo-template +2022-12-09T17:25:48.9362562Z Deleting the contents of '/home/runner/work/octo-template/octo-template' +2022-12-09T17:25:48.9366563Z ##[group]Initializing the repository +2022-12-09T17:25:48.9370913Z [command]/usr/bin/git init /home/runner/work/octo-template/octo-template +2022-12-09T17:25:48.9428579Z hint: Using 'master' as the name for the initial branch. This default branch name +2022-12-09T17:25:48.9429400Z hint: is subject to change. To configure the initial branch name to use in all +2022-12-09T17:25:48.9429828Z hint: of your new repositories, which will suppress this warning, call: +2022-12-09T17:25:48.9430089Z hint: +2022-12-09T17:25:48.9430525Z hint: git config --global init.defaultBranch <name> +2022-12-09T17:25:48.9430769Z hint: +2022-12-09T17:25:48.9431094Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and +2022-12-09T17:25:48.9431510Z hint: 'development'. The just-created branch can be renamed via this command: +2022-12-09T17:25:48.9431770Z hint: +2022-12-09T17:25:48.9431994Z hint: git branch -m <name> +2022-12-09T17:25:48.9442316Z Initialized empty Git repository in /home/runner/work/octo-template/octo-template/.git/ +2022-12-09T17:25:48.9454154Z [command]/usr/bin/git remote add origin https://github.com/octo-org/octo-template +2022-12-09T17:25:48.9490442Z ##[endgroup] +2022-12-09T17:25:48.9491112Z ##[group]Disabling automatic garbage collection +2022-12-09T17:25:48.9494676Z [command]/usr/bin/git config --local gc.auto 0 +2022-12-09T17:25:48.9524540Z ##[endgroup] +2022-12-09T17:25:48.9525308Z ##[group]Setting up auth +2022-12-09T17:25:48.9532488Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand +2022-12-09T17:25:48.9563906Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || : +2022-12-09T17:25:48.9857748Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/github\.com\/\.extraheader +2022-12-09T17:25:48.9892225Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || : +2022-12-09T17:25:49.0110192Z [command]/usr/bin/git config --local http.https://github.com/.extraheader AUTHORIZATION: basic *** +2022-12-09T17:25:49.0146631Z ##[endgroup] +2022-12-09T17:25:49.0147330Z ##[group]Fetching the repository +2022-12-09T17:25:49.0156010Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +refs/heads/main*:refs/remotes/origin/main* +refs/tags/main*:refs/tags/main* +2022-12-09T17:25:49.2837218Z remote: Enumerating objects: 3, done. +2022-12-09T17:25:49.2837524Z remote: Counting objects: 33% (1/3) +2022-12-09T17:25:49.2837809Z remote: Counting objects: 66% (2/3) +2022-12-09T17:25:49.2838044Z remote: Counting objects: 100% (3/3) +2022-12-09T17:25:49.2839733Z remote: Counting objects: 100% (3/3), done. +2022-12-09T17:25:49.2840661Z remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 +2022-12-09T17:25:49.2896737Z From https://github.com/octo-org/octo-template +2022-12-09T17:25:49.2897109Z * [new branch] main -> origin/main +2022-12-09T17:25:49.2924446Z ##[endgroup] +2022-12-09T17:25:49.2925129Z ##[group]Determining the checkout info +2022-12-09T17:25:49.2931526Z [command]/usr/bin/git branch --list --remote origin/main +2022-12-09T17:25:49.2958361Z origin/main +2022-12-09T17:25:49.2963283Z ##[endgroup] +2022-12-09T17:25:49.2964130Z ##[group]Checking out the ref +2022-12-09T17:25:49.2970560Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main +2022-12-09T17:25:49.3010381Z Switched to a new branch 'main' +2022-12-09T17:25:49.3014811Z branch 'main' set up to track 'origin/main'. +2022-12-09T17:25:49.3019861Z ##[endgroup] +2022-12-09T17:25:49.3022847Z ##[group]Setting up auth for fetching submodules +2022-12-09T17:25:49.3031486Z [command]/usr/bin/git config --global http.https://github.com/.extraheader AUTHORIZATION: basic *** +2022-12-09T17:25:49.3101009Z [command]/usr/bin/git config --global --unset-all url.https://github.com/.insteadOf +2022-12-09T17:25:49.3132496Z [command]/usr/bin/git config --global --add url.https://github.com/.insteadOf git@github.com: +2022-12-09T17:25:49.3159145Z ##[endgroup] +2022-12-09T17:25:49.3159671Z ##[group]Fetching submodules +2022-12-09T17:25:49.3163060Z [command]/usr/bin/git submodule sync --recursive +2022-12-09T17:25:49.3374628Z [command]/usr/bin/git -c protocol.version=2 submodule update --init --force --depth=1 --recursive +2022-12-09T17:25:49.3595435Z [command]/usr/bin/git submodule foreach --recursive git config --local gc.auto 0 +2022-12-09T17:25:49.3838526Z ##[endgroup] +2022-12-09T17:25:49.3839209Z ##[group]Persisting credentials for submodules +2022-12-09T17:25:49.3844491Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'url\.https\:\/\/github\.com\/\.insteadOf' && git config --local --unset-all 'url.https://github.com/.insteadOf' || : +2022-12-09T17:25:49.4113780Z [command]/usr/bin/git submodule foreach --recursive git config --local 'http.https://github.com/.extraheader' 'AUTHORIZATION: basic ***' && git config --local --show-origin --name-only --get-regexp remote.origin.url +2022-12-09T17:25:49.4324497Z [command]/usr/bin/git submodule foreach --recursive git config --local --add 'url.https://github.com/.insteadOf' 'git@github.com:' +2022-12-09T17:25:49.4536981Z ##[endgroup] +2022-12-09T17:25:49.4565161Z [command]/usr/bin/git log -1 --format='%H' +2022-12-09T17:25:49.4592051Z 'c85af0e5e5798047462143a13c1b455ee1275a64' +2022-12-09T17:25:49.4851246Z ##[group]Run actions/jekyll-build-pages@v1 +2022-12-09T17:25:49.4851491Z with: +2022-12-09T17:25:49.4851672Z source: . +2022-12-09T17:25:49.4851856Z destination: ./_site +2022-12-09T17:25:49.4852055Z future: false +2022-12-09T17:25:49.4852300Z build_revision: c85af0e5e5798047462143a13c1b455ee1275a64 +2022-12-09T17:25:49.4852525Z verbose: true +2022-12-09T17:25:49.4852887Z token: *** +2022-12-09T17:25:49.4853073Z ##[endgroup] +2022-12-09T17:25:49.5094807Z ##[command]/usr/bin/docker run --name ghcrioactionsjekyllbuildpagesv104_e36266 --label 290506 --workdir /github/workspace --rm -e "INPUT_SOURCE" -e "INPUT_DESTINATION" -e "INPUT_FUTURE" -e "INPUT_BUILD_REVISION" -e "INPUT_VERBOSE" -e "INPUT_TOKEN" -e "HOME" -e "GITHUB_JOB" -e "GITHUB_REF" -e "GITHUB_SHA" -e "GITHUB_REPOSITORY" -e "GITHUB_REPOSITORY_OWNER" -e "GITHUB_RUN_ID" -e "GITHUB_RUN_NUMBER" -e "GITHUB_RETENTION_DAYS" -e "GITHUB_RUN_ATTEMPT" -e "GITHUB_ACTOR" -e "GITHUB_TRIGGERING_ACTOR" -e "GITHUB_WORKFLOW" -e "GITHUB_HEAD_REF" -e "GITHUB_BASE_REF" -e "GITHUB_EVENT_NAME" -e "GITHUB_SERVER_URL" -e "GITHUB_API_URL" -e "GITHUB_GRAPHQL_URL" -e "GITHUB_REF_NAME" -e "GITHUB_REF_PROTECTED" -e "GITHUB_REF_TYPE" -e "GITHUB_WORKSPACE" -e "GITHUB_ACTION" -e "GITHUB_EVENT_PATH" -e "GITHUB_ACTION_REPOSITORY" -e "GITHUB_ACTION_REF" -e "GITHUB_PATH" -e "GITHUB_ENV" -e "GITHUB_STEP_SUMMARY" -e "GITHUB_STATE" -e "GITHUB_OUTPUT" -e "RUNNER_OS" -e "RUNNER_ARCH" -e "RUNNER_NAME" -e "RUNNER_TOOL_CACHE" -e "RUNNER_TEMP" -e "RUNNER_WORKSPACE" -e "ACTIONS_RUNTIME_URL" -e "ACTIONS_RUNTIME_TOKEN" -e "ACTIONS_CACHE_URL" -e "ACTIONS_ID_TOKEN_REQUEST_URL" -e "ACTIONS_ID_TOKEN_REQUEST_TOKEN" -e GITHUB_ACTIONS=true -e CI=true -v "/var/run/docker.sock":"/var/run/docker.sock" -v "/home/runner/work/_temp/_github_home":"/github/home" -v "/home/runner/work/_temp/_github_workflow":"/github/workflow" -v "/home/runner/work/_temp/_runner_file_commands":"/github/file_commands" -v "/home/runner/work/octo-template/octo-template":"/github/workspace" ghcr.io/actions/jekyll-build-pages:v1.0.4 +2022-12-09T17:25:50.5151506Z [33mConfiguration file: none[0m +2022-12-09T17:25:50.5313330Z Logging at level: debug +2022-12-09T17:25:50.5313941Z GitHub Pages: github-pages v227 +2022-12-09T17:25:50.5314484Z GitHub Pages: jekyll v3.9.2 +2022-12-09T17:25:50.5315560Z Theme: jekyll-theme-primer +2022-12-09T17:25:50.5316125Z Theme source: /usr/local/bundle/gems/jekyll-theme-primer-0.6.0 +2022-12-09T17:25:50.5316710Z Requiring: jekyll-github-metadata +2022-12-09T17:25:50.6063961Z To use retry middleware with Faraday v2.0+, install faraday-retry gem +2022-12-09T17:25:52.4164334Z Requiring: jekyll-seo-tag +2022-12-09T17:25:52.4165089Z Requiring: jekyll-coffeescript +2022-12-09T17:25:52.4166139Z Requiring: jekyll-commonmark-ghpages +2022-12-09T17:25:52.4166490Z Requiring: jekyll-gist +2022-12-09T17:25:52.4167025Z Requiring: jekyll-github-metadata +2022-12-09T17:25:52.4167363Z Requiring: jekyll-paginate +2022-12-09T17:25:52.4167663Z Requiring: jekyll-relative-links +2022-12-09T17:25:52.4167989Z Requiring: jekyll-optional-front-matter +2022-12-09T17:25:52.4168298Z Requiring: jekyll-readme-index +2022-12-09T17:25:52.4168589Z Requiring: jekyll-default-layout +2022-12-09T17:25:52.4168888Z Requiring: jekyll-titles-from-headings +2022-12-09T17:25:52.4169143Z GitHub Metadata: Initializing... +2022-12-09T17:25:52.4169392Z Source: /github/workspace/. +2022-12-09T17:25:52.4169651Z Destination: /github/workspace/./_site +2022-12-09T17:25:52.4169981Z Incremental build: disabled. Enable with --incremental +2022-12-09T17:25:52.4170230Z Generating... +2022-12-09T17:25:52.4183638Z Generating: JekyllOptionalFrontMatter::Generator finished in 0.000150897 seconds. +2022-12-09T17:25:52.4184136Z Generating: JekyllReadmeIndex::Generator finished in 0.00169567 seconds. +2022-12-09T17:25:52.4185038Z Generating: Jekyll::Paginate::Pagination finished in 4.8e-06 seconds. +2022-12-09T17:25:52.4185483Z Generating: JekyllRelativeLinks::Generator finished in 2.97e-05 seconds. +2022-12-09T17:25:52.4185914Z Generating: JekyllDefaultLayout::Generator finished in 7.7999e-05 seconds. +2022-12-09T17:25:52.4186258Z Requiring: kramdown-parser-gfm +2022-12-09T17:25:52.4186586Z Generating: JekyllTitlesFromHeadings::Generator finished in 0.008350253 seconds. +2022-12-09T17:25:52.4186888Z Rendering: assets/css/style.scss +2022-12-09T17:25:52.4187176Z Pre-Render Hooks: assets/css/style.scss +2022-12-09T17:25:52.4187437Z Rendering Markup: assets/css/style.scss +2022-12-09T17:25:52.4187672Z Rendering: README.md +2022-12-09T17:25:52.4187920Z Pre-Render Hooks: README.md +2022-12-09T17:25:52.4188141Z Rendering Markup: README.md +2022-12-09T17:25:52.4188360Z Rendering Layout: README.md +2022-12-09T17:25:52.4188568Z Layout source: theme +2022-12-09T17:25:52.4188886Z GitHub Metadata: Generating for octo-org/octo-template +2022-12-09T17:25:52.4189408Z GitHub Metadata: Calling @client.repository("octo-org/octo-template", {:accept=>"application/vnd.github.drax-preview+json"}) +2022-12-09T17:25:52.4189903Z GitHub Metadata: Calling @client.pages("octo-org/octo-template", {}) +2022-12-09T17:25:52.4190223Z Writing: /github/workspace/_site/assets/css/style.css +2022-12-09T17:25:52.4190520Z Writing: /github/workspace/_site/index.html +2022-12-09T17:25:52.4190772Z done in 1.524 seconds. +2022-12-09T17:25:52.4191074Z Auto-regeneration: disabled. Use --watch to enable. +2022-12-09T17:25:52.5460546Z ##[group]Run actions/upload-pages-artifact@v1 +2022-12-09T17:25:52.5460787Z with: +2022-12-09T17:25:52.5460968Z path: ./_site +2022-12-09T17:25:52.5461168Z name: github-pages +2022-12-09T17:25:52.5461361Z retention-days: 1 +2022-12-09T17:25:52.5461560Z ##[endgroup] +2022-12-09T17:25:52.5745795Z ##[group]Run chmod -c -R +rX . | while read line; do +2022-12-09T17:25:52.5746128Z [36;1mchmod -c -R +rX . | while read line; do[0m +2022-12-09T17:25:52.5746462Z [36;1m echo "::warning title=Invalid file permissions automatically fixed::$line"[0m +2022-12-09T17:25:52.5746740Z [36;1mdone[0m +2022-12-09T17:25:52.5746920Z [36;1mtar \[0m +2022-12-09T17:25:52.5747146Z [36;1m --dereference --hard-dereference \[0m +2022-12-09T17:25:52.5747410Z [36;1m --directory "$INPUT_PATH" \[0m +2022-12-09T17:25:52.5747668Z [36;1m -cvf "$RUNNER_TEMP/artifact.tar" \[0m +2022-12-09T17:25:52.5747896Z [36;1m --exclude=.git \[0m +2022-12-09T17:25:52.5748110Z [36;1m --exclude=.github \[0m +2022-12-09T17:25:52.5748352Z [36;1m .[0m +2022-12-09T17:25:52.5805770Z shell: /usr/bin/sh -e {0} +2022-12-09T17:25:52.5805980Z env: +2022-12-09T17:25:52.5806170Z INPUT_PATH: ./_site +2022-12-09T17:25:52.5806374Z ##[endgroup] +2022-12-09T17:25:52.5967973Z chmod: changing permissions of './_site': Operation not permitted +2022-12-09T17:25:52.5986749Z chmod: changing permissions of './_site/index.html': Operation not permitted +2022-12-09T17:25:52.5987217Z chmod: changing permissions of './_site/README.md': Operation not permitted +2022-12-09T17:25:52.5987625Z chmod: changing permissions of './_site/assets': Operation not permitted +2022-12-09T17:25:52.5988025Z chmod: changing permissions of './_site/assets/css': Operation not permitted +2022-12-09T17:25:52.5988455Z chmod: changing permissions of './_site/assets/css/style.css': Operation not permitted +2022-12-09T17:25:52.6003872Z ./ +2022-12-09T17:25:52.6004035Z ./index.html +2022-12-09T17:25:52.6004213Z ./README.md +2022-12-09T17:25:52.6004387Z ./assets/ +2022-12-09T17:25:52.6004556Z ./assets/css/ +2022-12-09T17:25:52.6004743Z ./assets/css/style.css +2022-12-09T17:25:52.6089697Z ##[group]Run actions/upload-artifact@main +2022-12-09T17:25:52.6089918Z with: +2022-12-09T17:25:52.6090104Z name: github-pages +2022-12-09T17:25:52.6090342Z path: /home/runner/work/_temp/artifact.tar +2022-12-09T17:25:52.6090677Z retention-days: 1 +2022-12-09T17:25:52.6090884Z if-no-files-found: warn +2022-12-09T17:25:52.6091085Z ##[endgroup] +2022-12-09T17:25:52.6745114Z With the provided path, there will be 1 file uploaded +2022-12-09T17:25:52.6748618Z Starting artifact upload +2022-12-09T17:25:52.6749367Z For more detailed logs during the artifact upload process, enable step-debugging: https://docs.github.com/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging#enabling-step-debug-logging +2022-12-09T17:25:52.6750012Z Artifact name is valid! +2022-12-09T17:25:52.7711407Z Container for artifact "github-pages" successfully created. Starting upload of file(s) +2022-12-09T17:25:52.8679144Z Total size of all the files uploaded is 12638 bytes +2022-12-09T17:25:52.8680162Z File upload process has finished. Finalizing the artifact upload +2022-12-09T17:25:52.8976033Z Artifact has been finalized. All files have been successfully uploaded! +2022-12-09T17:25:52.8976512Z +2022-12-09T17:25:52.8976816Z The raw size of all the files that were specified for upload is 92160 bytes +2022-12-09T17:25:52.8977329Z The size of all the files that were uploaded is 12638 bytes. This takes into account any gzip compression used to reduce the upload size, time and storage +2022-12-09T17:25:52.8977612Z +2022-12-09T17:25:52.8978179Z Note: The size of downloaded zips can differ significantly from the reported size. For more information see: https://github.com/actions/upload-artifact#zipped-artifact-downloads +2022-12-09T17:25:52.8978524Z +2022-12-09T17:25:52.8980509Z Artifact github-pages has been successfully uploaded! +2022-12-09T17:25:52.9084311Z Post job cleanup. +2022-12-09T17:25:53.0358449Z [command]/usr/bin/git version +2022-12-09T17:25:53.0407485Z git version 2.38.1 +2022-12-09T17:25:53.0465462Z Temporarily overriding HOME='/home/runner/work/_temp/52bccf8b-8b44-45e0-a383-0833ced20023' before making global git config changes +2022-12-09T17:25:53.0470560Z Adding repository directory to the temporary git global config as a safe directory +2022-12-09T17:25:53.0478850Z [command]/usr/bin/git config --global --add safe.directory /home/runner/work/octo-template/octo-template +2022-12-09T17:25:53.0517249Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand +2022-12-09T17:25:53.0550835Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || : +2022-12-09T17:25:53.0769173Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/github\.com\/\.extraheader +2022-12-09T17:25:53.0792441Z http.https://github.com/.extraheader +2022-12-09T17:25:53.0803756Z [command]/usr/bin/git config --local --unset-all http.https://github.com/.extraheader +2022-12-09T17:25:53.0838329Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || : +2022-12-09T17:25:53.1274888Z Cleaning up orphan processes +`; diff --git a/plugins/github-actions/src/__fixtures__/list-workflow-runs.json b/plugins/github-actions/src/__fixtures__/list-workflow-runs.json new file mode 100644 index 0000000000..4d1e1223ce --- /dev/null +++ b/plugins/github-actions/src/__fixtures__/list-workflow-runs.json @@ -0,0 +1,223 @@ +{ + "total_count": 1, + "workflow_runs": [ + { + "id": 30433642, + "name": "Build", + "node_id": "MDEyOldvcmtmbG93IFJ1bjI2OTI4OQ==", + "check_suite_id": 42, + "check_suite_node_id": "MDEwOkNoZWNrU3VpdGU0Mg==", + "head_branch": "master", + "head_sha": "acb5820ced9479c074f688cc328bf03f341a511d", + "run_number": 562, + "event": "push", + "status": "queued", + "conclusion": null, + "workflow_id": 159038, + "url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642", + "html_url": "https://github.com/octo-org/octo-repo/actions/runs/30433642", + "pull_requests": [], + "created_at": "2020-01-22T19:33:08Z", + "updated_at": "2020-01-22T19:33:08Z", + "actor": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "run_attempt": 1, + "run_started_at": "2020-01-22T19:33:08Z", + "triggering_actor": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/jobs", + "logs_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/logs", + "check_suite_url": "https://api.github.com/repos/octo-org/octo-repo/check-suites/414944374", + "artifacts_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/artifacts", + "cancel_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/cancel", + "rerun_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/rerun", + "workflow_url": "https://api.github.com/repos/octo-org/octo-repo/actions/workflows/159038", + "head_commit": { + "id": "acb5820ced9479c074f688cc328bf03f341a511d", + "tree_id": "d23f6eedb1e1b9610bbc754ddb5197bfe7271223", + "message": "Create linter.yaml", + "timestamp": "2020-01-22T19:33:05Z", + "author": { + "name": "Octo Cat", + "email": "octocat@github.com" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + }, + "repository": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks" + }, + "head_repository": { + "id": 217723378, + "node_id": "MDEwOlJlcG9zaXRvcnkyMTc3MjMzNzg=", + "name": "octo-repo", + "full_name": "octo-org/octo-repo", + "private": true, + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/octo-org/octo-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/octo-org/octo-repo", + "forks_url": "https://api.github.com/repos/octo-org/octo-repo/forks", + "keys_url": "https://api.github.com/repos/octo-org/octo-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/octo-org/octo-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/octo-org/octo-repo/teams", + "hooks_url": "https://api.github.com/repos/octo-org/octo-repo/hooks", + "issue_events_url": "https://api.github.com/repos/octo-org/octo-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/octo-org/octo-repo/events", + "assignees_url": "https://api.github.com/repos/octo-org/octo-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/octo-org/octo-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/octo-org/octo-repo/tags", + "blobs_url": "https://api.github.com/repos/octo-org/octo-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/octo-org/octo-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/octo-org/octo-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/octo-org/octo-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/octo-org/octo-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/octo-org/octo-repo/languages", + "stargazers_url": "https://api.github.com/repos/octo-org/octo-repo/stargazers", + "contributors_url": "https://api.github.com/repos/octo-org/octo-repo/contributors", + "subscribers_url": "https://api.github.com/repos/octo-org/octo-repo/subscribers", + "subscription_url": "https://api.github.com/repos/octo-org/octo-repo/subscription", + "commits_url": "https://api.github.com/repos/octo-org/octo-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/octo-org/octo-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/octo-org/octo-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/octo-org/octo-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/octo-org/octo-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/octo-org/octo-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/octo-org/octo-repo/merges", + "archive_url": "https://api.github.com/repos/octo-org/octo-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/octo-org/octo-repo/downloads", + "issues_url": "https://api.github.com/repos/octo-org/octo-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/octo-org/octo-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/octo-org/octo-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octo-org/octo-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/octo-org/octo-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/octo-org/octo-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/octo-org/octo-repo/deployments" + } + } + ] +} diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx index 5e0a5f3e15..f55cad8424 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -94,7 +94,7 @@ describe('<RecentWorkflowRunsCard />', () => { ); it('renders a table with a row for each workflow', async () => { - const subject = await renderSubject(); + const subject = renderSubject(); workflowRuns.forEach(run => { expect(subject.getByText(run.message)).toBeInTheDocument(); @@ -102,7 +102,7 @@ describe('<RecentWorkflowRunsCard />', () => { }); it('renders a workflow row correctly', async () => { - const subject = await renderSubject(); + const subject = renderSubject(); const [run] = workflowRuns; expect(subject.getByText(run.message).closest('a')).toHaveAttribute( 'href', diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index f55803fc5c..d073a5eed9 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-github-deployments +## 0.1.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + +## 0.1.46 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + +## 0.1.46-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + +## 0.1.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/integration-react@1.1.9 + ## 0.1.45 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 523a7af480..9027c5f23a 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.45", + "version": "0.1.47-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/github-issues/CHANGELOG.md b/plugins/github-issues/CHANGELOG.md index 5ba471e4ad..9e4e64b628 100644 --- a/plugins/github-issues/CHANGELOG.md +++ b/plugins/github-issues/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-github-issues +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## 0.2.4 + +### Patch Changes + +- 3f88ae9d0a: Updated README.md examples to use correct components and fixed some syntax errors. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## 0.2.4-next.0 + +### Patch Changes + +- 3f88ae9d0a: Updated README.md examples to use correct components and fixed some syntax errors. +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.2.3 ### Patch Changes diff --git a/plugins/github-issues/README.md b/plugins/github-issues/README.md index d35f2c5d3f..f6b383bfcc 100644 --- a/plugins/github-issues/README.md +++ b/plugins/github-issues/README.md @@ -25,8 +25,8 @@ After installation, the plugin can be used as a Card or as a Page. ```typescript import { - GitHubIssuesCard, - GitHubIssuesPage, + GithubIssuesCard, + GithubIssuesPage, } from '@backstage/plugin-github-issues'; // To use as a page Plugin needs to be wrapped in EntityLayout.Route @@ -34,9 +34,9 @@ const RenderGitHubIssuesPage = () => ( <EntityLayoutWrapper> <EntityLayout.Route path="/" title="Overview"> <EntityLayout.Route path="github-issues" title="GitHub Issues"> - <GitHubIssuesPage /> + <GithubIssuesPage /> </EntityLayout.Route> - <EntityLayout.Route /> + </EntityLayout.Route> </EntityLayoutWrapper> ); @@ -46,17 +46,17 @@ const RenderGitHubIssuesCard = () => ( <EntityLayout.Route path="/" title="Overview"> <Grid container spacing={3}> <Grid item xs={12}> - <GitHubIssuesCard /> + <GithubIssuesCard /> </Grid> </Grid> - <EntityLayout.Route /> + </EntityLayout.Route> </EntityLayoutWrapper> ); ``` ## Configuration -Both `GitHubIssuesPage` and `GitHubIssuesCard` provide default configuration. It is ready to use out of the box. +Both `GithubIssuesPage` and `GithubIssuesCard` provide default configuration. It is ready to use out of the box. However, you can configure the plugin with props: - `itemsPerPage: number = 10` - Issues in the list are paginated, number of issues on a single page is controlled with this prop @@ -67,7 +67,7 @@ However, you can configure the plugin with props: ### `filterBy` and `orderBy` example ```ts -<GitHubIssuesCard +<GithubIssuesCard filterBy={{ labels: ['bug', 'enhancement'], states: ['OPEN'], diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json index 655818f164..d891951537 100644 --- a/plugins/github-issues/package.json +++ b/plugins/github-issues/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-issues", - "version": "0.2.3", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@types/node": "*", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/github-pull-requests-board/CHANGELOG.md b/plugins/github-pull-requests-board/CHANGELOG.md index 6681f1f7c7..ec4da0406e 100644 --- a/plugins/github-pull-requests-board/CHANGELOG.md +++ b/plugins/github-pull-requests-board/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-github-pull-requests-board +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.17 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/integration@1.4.2 + +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json index 01657b7b93..e837da56e9 100644 --- a/plugins/github-pull-requests-board/package.json +++ b/plugins/github-pull-requests-board/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-pull-requests-board", "description": "A Backstage plugin that allows you to see all open Pull Requests for all the repositories owned by your team", - "version": "0.1.8", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -58,7 +58,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index c7e895315a..b6ef8bfede 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-gitops-profiles +## 0.3.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.3.33 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## 0.3.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.3.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.3.32 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 9c8f623c12..57d8f2ae34 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.32", + "version": "0.3.34-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -56,7 +56,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md index 2cc84e0b68..2d576fa79b 100644 --- a/plugins/gocd/CHANGELOG.md +++ b/plugins/gocd/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-gocd +## 0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## 0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json index 684737f2b9..4567a6e59b 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.20", + "version": "0.1.22-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -58,7 +58,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index f14c0602d0..0ee2c37177 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-graphiql +## 0.2.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.2.47 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## 0.2.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.2.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.2.46 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 92c89dda60..7f1bd12beb 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.46", + "version": "0.2.48-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -58,7 +58,7 @@ "@types/codemirror": "^5.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/graphql-backend/CHANGELOG.md b/plugins/graphql-backend/CHANGELOG.md index 7365acdee1..3497d58b44 100644 --- a/plugins/graphql-backend/CHANGELOG.md +++ b/plugins/graphql-backend/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-graphql-backend +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.19-next.0 + +## 0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.18 + +## 0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.18-next.1 + +## 0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.18-next.0 + +## 0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-catalog-graphql@0.3.18-next.0 + ## 0.1.30 ### Patch Changes diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index e2d0f854c0..11e69c3eae 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.30", + "version": "0.1.33-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -52,7 +52,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/graphql-voyager/.eslintrc.js b/plugins/graphql-voyager/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/graphql-voyager/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/graphql-voyager/CHANGELOG.md b/plugins/graphql-voyager/CHANGELOG.md new file mode 100644 index 0000000000..678cec72b2 --- /dev/null +++ b/plugins/graphql-voyager/CHANGELOG.md @@ -0,0 +1,40 @@ +# @backstage/plugin-graphql-voyager + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.1.0 + +### Minor Changes + +- d1fb4b7bf1: Add the new GraphQL Voyager tool as a plugin for backstage, expose necessary API and components needed for it. + + For more information on how to use this plugin and configure it, please navigate to the [README](https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager/README.md) of the plugin. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## 0.1.0-next.0 + +### Minor Changes + +- d1fb4b7bf1: Add the new GraphQL Voyager tool as a plugin for backstage, expose necessary API and components needed for it. + + For more information on how to use this plugin and configure it, please navigate to the [README](https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager/README.md) of the plugin. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 diff --git a/plugins/graphql-voyager/README.md b/plugins/graphql-voyager/README.md new file mode 100644 index 0000000000..2c903cd9e5 --- /dev/null +++ b/plugins/graphql-voyager/README.md @@ -0,0 +1,76 @@ +# graphql-voyager + +Welcome to the graphql-voyager plugin! + +Presents a graph structure of your entire GraphQL API, with tabs for multiple API urls: +![](./src/assets/plugin.png) + +## Getting started + +### Installing + +To get started, first install the plugin with the following command: + +```bash +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-graphql-voyager +``` + +### Adding the page + +In order to be able to navigate to the graphQL voyager page, a new route needs to be added to the app. This can be done in the following way: + +```tsx +// packages/app/App.tsx + + import { GraphQLVoyagerPage } from '@backstage/plugin-graphql-voyager'; + + const routes = ( + <FlatRoutes> + <Route path="/graphql-voyager" element={<GraphqlVoyagerPage title="This is Voyager!"/>}/> +``` + +### Configuration + +In order for the plugin to function correctly. GraphQL endpoints need to be added / configured through the GraphQLVoyager API. This can be done by implementing the `graphQlVoyagerApiRef` exported by this plugin. + +Add your own configuration to the `packages/app/src/apis.ts` file the following way: + +```ts +import { identityApiRef } from '@backstage/core-plugin-api'; +import { GraphQLVoyagerEndpoints } from '@backstage/plugin-graphql-voyager'; + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: graphQlVoyagerApiRef, + deps: { identityApi: identityApiRef }, + factory: ({ identityApiRef }) => { + return GraphQLVoyagerEndpoints.from([ + { + id: `graphql-voyager-endpoint-id`, + title: 'endpoint-title', + introspectionErrorMessage: + 'Unable to perform introspection, make sure you are on the correct environment.', + introspection: async (query: any) => { + const token = 'someSecretJWTComingFromIdentityApiRef'; + + const res = await fetch('graphQLEndpoint', { + method: 'POST', + body: JSON.stringify({ query }), + headers: { + 'Content-Type': 'application/json', + Authorization: token, + }, + }); + + return res.json(); + }, + voyagerProps: { + hideDocs: true, + }, + }, + ]); + }, + }), +]; +``` diff --git a/plugins/graphql-voyager/api-report.md b/plugins/graphql-voyager/api-report.md new file mode 100644 index 0000000000..9b488f9356 --- /dev/null +++ b/plugins/graphql-voyager/api-report.md @@ -0,0 +1,71 @@ +## API Report File for "@backstage/plugin-graphql-voyager" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="react" /> + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { VoyagerDisplayOptions } from 'graphql-voyager/typings/components/Voyager'; +import { WorkerCallback } from 'graphql-voyager/typings/utils/types'; + +// @public (undocumented) +export type GraphQLVoyagerApi = { + getEndpoints(): Promise<GraphQLVoyagerEndpoint[]>; +}; + +// @public (undocumented) +export const graphQlVoyagerApiRef: ApiRef<GraphQLVoyagerApi>; + +// @public (undocumented) +export type GraphQLVoyagerEndpoint = { + id: string; + title: string; + introspection: (body: any) => Promise<any>; + introspectionErrorMessage: string; + voyagerProps?: { + displayOptions?: VoyagerDisplayOptions; + hideDocs?: boolean; + hideSettings?: boolean; + workerURI?: string; + loadWorker?: WorkerCallback; + }; +}; + +// @public (undocumented) +export class GraphQLVoyagerEndpoints implements GraphQLVoyagerApi { + // (undocumented) + static from(endpoints: GraphQLVoyagerEndpoint[]): GraphQLVoyagerEndpoints; + // (undocumented) + getEndpoints(): Promise<GraphQLVoyagerEndpoint[]>; +} + +// @public (undocumented) +export const GraphqlVoyagerPage: ( + props: GraphQLVoyagerPageProps, +) => JSX.Element; + +// @public (undocumented) +export type GraphQLVoyagerPageProps = { + title?: string; +}; + +// @public (undocumented) +export const graphqlVoyagerPlugin: BackstagePlugin< + { + root: RouteRef<undefined>; + }, + {}, + {} +>; + +// @public (undocumented) +export const introspectionQuery: string; + +// @public (undocumented) +export const Router: (props: GraphQLVoyagerPageProps) => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/graphql-voyager/package.json b/plugins/graphql-voyager/package.json new file mode 100644 index 0000000000..3f6d833f7f --- /dev/null +++ b/plugins/graphql-voyager/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-graphql-voyager", + "description": "Backstage plugin for GraphQL Voyager", + "version": "0.1.1-next.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "graphql-voyager": "^1.0.0-rc.31", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/graphql-voyager/src/assets/plugin.png b/plugins/graphql-voyager/src/assets/plugin.png new file mode 100644 index 0000000000..602c7abde3 Binary files /dev/null and b/plugins/graphql-voyager/src/assets/plugin.png differ diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.test.tsx b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.test.tsx new file mode 100644 index 0000000000..76ccbd9965 --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { GraphQLVoyagerBrowser } from './GraphQLVoyagerBrowser'; + +jest.mock('graphql-voyager', () => ({ Voyager: () => '<Voyager />' })); + +describe('GraphQLVoyagerBrowser', () => { + it('should render error text if there are no endpoints', async () => { + const rendered = await renderInTestApp( + <GraphQLVoyagerBrowser endpoints={[]} />, + ); + + expect(rendered.getByText('No endpoints available')).toBeInTheDocument(); + }); + + it('should render endpoint tabs', async () => { + const rendered = await renderInTestApp( + <GraphQLVoyagerBrowser + endpoints={[ + { + id: 'a', + title: 'Endpoint A', + introspectionErrorMessage: 'Failed to introspect A', + async introspection() { + return { + data: { + _schema: {}, + }, + }; + }, + voyagerProps: { + hideDocs: true, + }, + }, + { + id: 'b', + title: 'Endpoint B', + introspectionErrorMessage: 'Failed to introspect B', + async introspection() { + return { + data: { + _schema: {}, + }, + }; + }, + voyagerProps: { + hideDocs: true, + }, + }, + ]} + />, + ); + + expect(rendered.getByText('Endpoint A')).toBeInTheDocument(); + expect(rendered.getByText('Endpoint B')).toBeInTheDocument(); + }); + + it('Should render the introspection error message when introspection fails', async () => { + const rendered = await renderInTestApp( + <GraphQLVoyagerBrowser + endpoints={[ + { + id: 'a', + title: 'Endpoint A', + introspectionErrorMessage: 'Failed to introspect A', + async introspection() { + return {}; + }, + voyagerProps: { + hideDocs: true, + }, + }, + ]} + />, + ); + + expect(rendered.getByText('Failed to introspect A')).toBeInTheDocument(); + }); +}); diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.tsx b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.tsx new file mode 100644 index 0000000000..d2f9262983 --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + GraphQLVoyagerEndpoint, + introspectionQuery, +} from '../../lib/api/types'; +import { useState } from 'react'; +import { makeStyles, Tab, Tabs, Typography } from '@material-ui/core'; +import React, { Suspense } from 'react'; +import { BackstageTheme } from '@backstage/theme'; +import { Content, ErrorPanel, Progress } from '@backstage/core-components'; +import { Voyager } from 'graphql-voyager'; +import useAsync from 'react-use/lib/useAsync'; + +const useStyles = makeStyles<BackstageTheme>(() => ({ + root: { + height: '100%', + display: 'flex', + flexFlow: 'column nowrap', + }, +})); + +type GraphQLVoyagerBrowserProps = { + endpoints: GraphQLVoyagerEndpoint[]; +}; + +const NoEndpoints = () => { + return <Typography variant="h4">No endpoints available</Typography>; +}; + +const VoyagerBrowserContent = ({ + endpoint, +}: { + endpoint: GraphQLVoyagerEndpoint; +}) => { + const { id, introspection, introspectionErrorMessage, voyagerProps } = + endpoint; + const { value, loading, error } = useAsync( + () => introspection(introspectionQuery), + [endpoint], + ); + + if (loading) { + return <Progress />; + } else if (error) { + return <ErrorPanel error={error} />; + } + return value.data ? ( + <Voyager key={id} introspection={value} {...voyagerProps} /> + ) : ( + <Typography variant="h4" color="error"> + {introspectionErrorMessage} + </Typography> + ); +}; + +const VoyagerBrowser = (props: GraphQLVoyagerBrowserProps) => { + const { endpoints } = props; + const [tabIndex, setTabIndex] = useState(0); + const classes = useStyles(); + + return ( + <div className={classes.root}> + <Suspense fallback={<Progress />}> + <Tabs + value={tabIndex} + onChange={(_, value) => setTabIndex(value)} + indicatorColor="primary" + > + {endpoints.map(({ title, id }, index) => ( + <Tab key={id} label={title} value={index} /> + ))} + </Tabs> + <Content> + <VoyagerBrowserContent endpoint={endpoints[tabIndex]} /> + </Content> + </Suspense> + </div> + ); +}; + +/** @public */ +export const GraphQLVoyagerBrowser = (props: GraphQLVoyagerBrowserProps) => { + const hasEndpoints = checkEndpoints(props); + + if (!hasEndpoints) { + return <NoEndpoints />; + } + return <VoyagerBrowser {...props} />; +}; + +function checkEndpoints(props: GraphQLVoyagerBrowserProps) { + return props.endpoints.length; +} diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/index.ts b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/index.ts new file mode 100644 index 0000000000..3beb0ccd95 --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './GraphQLVoyagerBrowser'; diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.test.tsx b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.test.tsx new file mode 100644 index 0000000000..67c257be2b --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; + +import { GraphQLVoyagerPage } from './GraphQLVoyagerPage'; +import { GraphQLVoyagerApi, graphQlVoyagerApiRef } from '../../lib/api'; + +jest.mock('../GraphQLVoyagerBrowser', () => ({ + GraphQLVoyagerBrowser: () => '<GraphQLVoyagerBrowser />', +})); + +describe('GraphQLVoyagerPage', () => { + it('should show error when no endpoints are loaded', async () => { + const loadingApi: GraphQLVoyagerApi = { + async getEndpoints() { + throw new Error('No endpoints'); + }, + }; + + const rendered = await renderInTestApp( + <TestApiProvider apis={[[graphQlVoyagerApiRef, loadingApi]]}> + <GraphQLVoyagerPage /> + </TestApiProvider>, + ); + + expect(rendered.getByText('Welcome to Voyager!')).toBeInTheDocument(); + expect(rendered.getByText('Error: No endpoints')).toBeInTheDocument(); + }); + + it('should show GraphQLVoyagerBrowser', async () => { + const loadingApi: GraphQLVoyagerApi = { + async getEndpoints() { + return []; + }, + }; + + const rendered = await renderInTestApp( + <TestApiProvider apis={[[graphQlVoyagerApiRef, loadingApi]]}> + <GraphQLVoyagerPage /> + </TestApiProvider>, + ); + + expect(rendered.getByText('Welcome to Voyager!')).toBeInTheDocument(); + expect(rendered.getByText('<GraphQLVoyagerBrowser />')).toBeInTheDocument(); + }); +}); diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.tsx b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.tsx new file mode 100644 index 0000000000..f3c4f70b4f --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + Header, + Page, + Content, + Progress, + ErrorPanel, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import 'graphql-voyager/dist/voyager.css'; +import { graphQlVoyagerApiRef } from '../../lib/api'; +import useAsync from 'react-use/lib/useAsync'; +import { GraphQLVoyagerBrowser } from '../GraphQLVoyagerBrowser'; + +/** @public */ +export type GraphQLVoyagerPageProps = { + title?: string; +}; + +const VoyagerContent = () => { + const graphQLVoyagerApi = useApi(graphQlVoyagerApiRef); + + const { value, loading, error } = useAsync(() => + graphQLVoyagerApi.getEndpoints(), + ); + + if (loading) { + return <Progress />; + } else if (error) { + return <ErrorPanel error={error} />; + } + return <GraphQLVoyagerBrowser endpoints={value!} />; +}; + +/** @public */ +export const GraphQLVoyagerPage = (props: GraphQLVoyagerPageProps) => { + const { title = 'Welcome to Voyager!' } = props; + + return ( + <Page themeId="tool"> + <Header title={title} /> + <Content> + <VoyagerContent /> + </Content> + </Page> + ); +}; diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/index.ts b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/index.ts new file mode 100644 index 0000000000..5ee042fd5f --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './GraphQLVoyagerPage'; diff --git a/plugins/graphql-voyager/src/components/index.ts b/plugins/graphql-voyager/src/components/index.ts new file mode 100644 index 0000000000..76c4229839 --- /dev/null +++ b/plugins/graphql-voyager/src/components/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './GraphQLVoyagerPage'; +export * from './GraphQLVoyagerBrowser'; diff --git a/plugins/graphql-voyager/src/index.ts b/plugins/graphql-voyager/src/index.ts new file mode 100644 index 0000000000..9e314c2eef --- /dev/null +++ b/plugins/graphql-voyager/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { graphqlVoyagerPlugin, GraphqlVoyagerPage } from './plugin'; +export { GraphQLVoyagerPage as Router } from './components'; +export type { GraphQLVoyagerPageProps } from './components'; +export * from './lib/api'; diff --git a/plugins/graphql-voyager/src/lib/api/GraphQLVoyagerEndpoints.ts b/plugins/graphql-voyager/src/lib/api/GraphQLVoyagerEndpoints.ts new file mode 100644 index 0000000000..4d08ffbce3 --- /dev/null +++ b/plugins/graphql-voyager/src/lib/api/GraphQLVoyagerEndpoints.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { GraphQLVoyagerApi, GraphQLVoyagerEndpoint } from './types'; + +/** @public */ +export class GraphQLVoyagerEndpoints implements GraphQLVoyagerApi { + private constructor(private readonly endpoints: GraphQLVoyagerEndpoint[]) {} + + static from(endpoints: GraphQLVoyagerEndpoint[]) { + return new GraphQLVoyagerEndpoints(endpoints); + } + + async getEndpoints() { + return this.endpoints; + } +} diff --git a/plugins/graphql-voyager/src/lib/api/index.ts b/plugins/graphql-voyager/src/lib/api/index.ts new file mode 100644 index 0000000000..c52eedafb7 --- /dev/null +++ b/plugins/graphql-voyager/src/lib/api/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './GraphQLVoyagerEndpoints'; +export * from './types'; diff --git a/plugins/graphql-voyager/src/lib/api/types.ts b/plugins/graphql-voyager/src/lib/api/types.ts new file mode 100644 index 0000000000..08e6f13923 --- /dev/null +++ b/plugins/graphql-voyager/src/lib/api/types.ts @@ -0,0 +1,141 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createApiRef } from '@backstage/core-plugin-api'; +import { VoyagerDisplayOptions } from 'graphql-voyager/typings/components/Voyager'; +import { WorkerCallback } from 'graphql-voyager/typings/utils/types'; + +/** @public */ +export type GraphQLVoyagerEndpoint = { + id: string; + title: string; + introspection: (body: any) => Promise<any>; + introspectionErrorMessage: string; + voyagerProps?: { + displayOptions?: VoyagerDisplayOptions; + hideDocs?: boolean; + hideSettings?: boolean; + workerURI?: string; + loadWorker?: WorkerCallback; + }; +}; + +/** @public */ +export type GraphQLVoyagerApi = { + getEndpoints(): Promise<GraphQLVoyagerEndpoint[]>; +}; + +/** @public */ +export const graphQlVoyagerApiRef = createApiRef<GraphQLVoyagerApi>({ + id: 'plugin.graphqlvoyager.api', +}); + +/** @public */ +export const introspectionQuery: string = `query IntrospectionQuery { + __schema { + + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + + locations + args { + ...InputValue + } + } + } + } + + fragment FullType on __Type { + kind + name + description + + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + + + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + }`; diff --git a/plugins/graphql-voyager/src/plugin.test.ts b/plugins/graphql-voyager/src/plugin.test.ts new file mode 100644 index 0000000000..df7acc7143 --- /dev/null +++ b/plugins/graphql-voyager/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { graphqlVoyagerPlugin } from './plugin'; + +describe('graphql-voyager', () => { + it('should export plugin', () => { + expect(graphqlVoyagerPlugin).toBeDefined(); + }); +}); diff --git a/plugins/graphql-voyager/src/plugin.ts b/plugins/graphql-voyager/src/plugin.ts new file mode 100644 index 0000000000..bd33209a1d --- /dev/null +++ b/plugins/graphql-voyager/src/plugin.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +/** @public */ +export const graphqlVoyagerPlugin = createPlugin({ + id: 'graphql-voyager', + routes: { + root: rootRouteRef, + }, +}); + +/** @public */ +export const GraphqlVoyagerPage = graphqlVoyagerPlugin.provide( + createRoutableExtension({ + name: 'GraphqlVoyagerPage', + component: () => import('./components').then(m => m.GraphQLVoyagerPage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/graphql-voyager/src/routes.ts b/plugins/graphql-voyager/src/routes.ts new file mode 100644 index 0000000000..12f06c1cb5 --- /dev/null +++ b/plugins/graphql-voyager/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'graphql-voyager', +}); diff --git a/plugins/graphql-voyager/src/setupTests.ts b/plugins/graphql-voyager/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/graphql-voyager/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 968a5870c4..c28cc9159a 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-home +## 0.4.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.4.31 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 3d1d867d42: Fixed regression that caused the `WelcomeTitle` to not be the right size when passed to the `title` property of the `<Header>` component. A Storybook entry was also added for the `WelcomeTitle` +- c553a625d2: remove unused plugin-stack-overflow dependency +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + +## 0.4.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.4.31-next.1 + +### Patch Changes + +- 3d1d867d42: Fixed regression that caused the `WelcomeTitle` to not be the right size when passed to the `title` property of the `<Header>` component. A Storybook entry was also added for the `WelcomeTitle` +- c553a625d2: remove unused plugin-stack-overflow dependency +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.4.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-stack-overflow@0.1.11-next.0 + ## 0.4.30 ### Patch Changes diff --git a/plugins/home/README.md b/plugins/home/README.md index 0cd307b706..1fdface722 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -9,7 +9,7 @@ For App Integrators, the system is designed to be composable to give total freed If you have a standalone app (you didn't clone this repo), then do ```bash -# From the Backstage repository root +# From your Backstage root directory yarn add --cwd packages/app @backstage/plugin-home ``` diff --git a/plugins/home/package.json b/plugins/home/package.json index 7fe4068947..f7f98a26fd 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.30", + "version": "0.4.32-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,7 +38,6 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", - "@backstage/plugin-stack-overflow": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -61,7 +60,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx new file mode 100644 index 0000000000..ddcce2fe41 --- /dev/null +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Header } from '@backstage/core-components'; +import { wrapInTestApp } from '@backstage/test-utils'; +import React, { ComponentType } from 'react'; +import { WelcomeTitle } from './WelcomeTitle'; + +export default { + title: 'Plugins/Home/Components/WelcomeTitle', + decorators: [(Story: ComponentType<{}>) => wrapInTestApp(<Story />)], +}; + +export const Default = () => { + return <Header title={<WelcomeTitle />} pageTitleOverride="Home" />; +}; diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx index 1d2f3d672a..49ae38d90c 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx @@ -18,8 +18,7 @@ import { identityApiRef, useApi, } from '@backstage/core-plugin-api'; -import { Tooltip } from '@material-ui/core'; -import Typography from '@material-ui/core/Typography'; +import { Tooltip, Typography } from '@material-ui/core'; import React, { useEffect, useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { getTimeBasedGreeting } from './timeUtil'; @@ -44,7 +43,7 @@ export const WelcomeTitle = () => { return ( <Tooltip title={greeting.language}> - <Typography component="span">{`${greeting.greeting}${ + <Typography component="span" variant="inherit">{`${greeting.greeting}${ profile?.displayName ? `, ${profile?.displayName}` : '' }!`}</Typography> </Tooltip> diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index c3f33fc4f4..18025f037b 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-ilert +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.2.4 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.2.3 ### Patch Changes diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index c704108126..814baea507 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -26,7 +26,7 @@ In detail this plugin provides: Install the plugin: ```bash -# From the Backstage repository root +# From your Backstage root directory yarn add --cwd packages/app @backstage/plugin-ilert ``` diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index fffbcbc16e..1bf968218a 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.2.3", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 8925ad532f..e9405af8a1 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-jenkins-backend +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-jenkins-common@0.1.14-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## 0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-jenkins-common@0.1.13 + - @backstage/plugin-permission-common@0.7.3 + +## 0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-jenkins-common@0.1.13-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## 0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-jenkins-common@0.1.13-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## 0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-jenkins-common@0.1.13-next.0 + ## 0.1.30 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index ecddb43149..b9a421d408 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.30", + "version": "0.1.33-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "@backstage/cli": "workspace:^", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/jenkins-common/CHANGELOG.md b/plugins/jenkins-common/CHANGELOG.md index 82307ff247..dd692185f2 100644 --- a/plugins/jenkins-common/CHANGELOG.md +++ b/plugins/jenkins-common/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-jenkins-common +## 0.1.14-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## 0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.11-next.0 + ## 0.1.12 ### Patch Changes diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json index 0950759950..b745c50fa2 100644 --- a/plugins/jenkins-common/package.json +++ b/plugins/jenkins-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins-common", - "version": "0.1.12", + "version": "0.1.14-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins-common/src/permissions.ts b/plugins/jenkins-common/src/permissions.ts index 5ca5ec6adf..311bf993cc 100644 --- a/plugins/jenkins-common/src/permissions.ts +++ b/plugins/jenkins-common/src/permissions.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { createPermission } from '@backstage/plugin-permission-common'; /** diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index e562f6b598..58dfae66d4 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-jenkins +## 0.7.15-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-jenkins-common@0.1.14-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.7.14 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-jenkins-common@0.1.13 + +## 0.7.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-jenkins-common@0.1.13-next.1 + +## 0.7.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-jenkins-common@0.1.13-next.0 + +## 0.7.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-jenkins-common@0.1.13-next.0 + ## 0.7.13 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index b82ed97e08..f6eb791af0 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.7.13", + "version": "0.7.15-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -61,7 +61,7 @@ "@types/node": "^16.11.26", "@types/testing-library__jest-dom": "^5.9.1", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx index 0217f00513..1bb6dbaefc 100644 --- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -15,7 +15,7 @@ */ import { Link, Progress, Table, TableColumn } from '@backstage/core-components'; import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { useEntityPermission } from '@backstage/plugin-catalog-react'; +import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha'; import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; import { default as React, useState } from 'react'; diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index 4800c3161e..89e8babe2e 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-kafka-backend +## 0.2.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.35 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.35-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + ## 0.2.33 ### Patch Changes diff --git a/plugins/kafka-backend/api-report.md b/plugins/kafka-backend/api-report.md index 20c8260b52..a748a543d9 100644 --- a/plugins/kafka-backend/api-report.md +++ b/plugins/kafka-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 { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -10,6 +11,9 @@ import { Logger } from 'winston'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise<express.Router>; +// @alpha +export const kafkaPlugin: () => BackendFeature; + // @public (undocumented) export interface RouterOptions { // (undocumented) diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index db3ffd6dde..33f5b667c6 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,14 +1,15 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.33", + "version": "0.2.36-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "backend-plugin" @@ -26,7 +27,7 @@ "configSchema": "config.d.ts", "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -35,6 +36,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", @@ -54,6 +56,7 @@ }, "files": [ "dist", - "config.d.ts" + "config.d.ts", + "alpha" ] } diff --git a/plugins/kafka-backend/src/index.ts b/plugins/kafka-backend/src/index.ts index 70b7fb45a4..2000c8b89a 100644 --- a/plugins/kafka-backend/src/index.ts +++ b/plugins/kafka-backend/src/index.ts @@ -22,3 +22,4 @@ export type { RouterOptions } from './service/router'; export { createRouter } from './service/router'; +export { kafkaPlugin } from './plugin'; diff --git a/plugins/kafka-backend/src/plugin.ts b/plugins/kafka-backend/src/plugin.ts new file mode 100644 index 0000000000..2442eddbaa --- /dev/null +++ b/plugins/kafka-backend/src/plugin.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The Kafka backend plugin. + * + * @alpha + */ +export const kafkaPlugin = createBackendPlugin({ + pluginId: 'kafka', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ config, logger, httpRouter }) { + httpRouter.use( + await createRouter({ config, logger: loggerToWinstonLogger(logger) }), + ); + }, + }); + }, +}); diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index f5f59c6e15..8f0afda96d 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-kafka +## 0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + +## 0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index e9ee53d3ad..98f01102f3 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.3.14", + "version": "0.3.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", "jest-when": "^3.1.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index bb212a7607..26ad74cc1e 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,79 @@ # @backstage/plugin-kubernetes-backend +## 0.9.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-kubernetes-common@0.6.1-next.0 + +## 0.9.3 + +### Patch Changes + +- 2518ef5b8a: Adding new Cluster detail fields to catalogClusterLocator. Replace deprecated imports with k8s annotations from plugin-kubernetes-common. +- 7ff81f7692: Kubernetes proxy endpoint now accepts content types that are not json +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- a53d06afe5: The name of the header used to specify a cluster to the proxy endpoint is now visible in the API reference. +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + +## 0.9.3-next.2 + +### Patch Changes + +- 7ff81f7692: Kubernetes proxy endpoint now accepts content types that are not json +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-kubernetes-common@0.6.0-next.2 + +## 0.9.3-next.1 + +### Patch Changes + +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/plugin-kubernetes-common@0.6.0-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## 0.9.3-next.0 + +### Patch Changes + +- 2518ef5b8a: Adding new Cluster detail fields to catalogClusterLocator. Replace deprecated imports with k8s annotations from plugin-kubernetes-common. +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + ## 0.9.1 ### Patch Changes diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 4ec043a8f9..a6a4bcfc58 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -21,7 +21,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import type { RequestHandler } from 'express'; import { TokenCredential } from '@azure/identity'; -// @alpha (undocumented) +// @public (undocumented) export interface AWSClusterDetails extends ClusterDetails { // (undocumented) assumeRole?: string; @@ -29,7 +29,7 @@ export interface AWSClusterDetails extends ClusterDetails { externalId?: string; } -// @alpha (undocumented) +// @public (undocumented) export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator { @@ -54,10 +54,10 @@ export class AwsIamKubernetesAuthTranslator validCredentials(creds: SigningCreds): boolean; } -// @alpha (undocumented) +// @public (undocumented) export interface AzureClusterDetails extends ClusterDetails {} -// @alpha (undocumented) +// @public (undocumented) export class AzureIdentityKubernetesAuthTranslator implements KubernetesAuthTranslator { @@ -68,7 +68,7 @@ export class AzureIdentityKubernetesAuthTranslator ): Promise<AzureClusterDetails>; } -// @alpha (undocumented) +// @public (undocumented) export interface ClusterDetails { // (undocumented) authProvider: string; @@ -91,25 +91,25 @@ export interface ClusterDetails { url: string; } -// @alpha @deprecated +// @public @deprecated export function createRouter(options: RouterOptions): Promise<express.Router>; -// @alpha (undocumented) +// @public (undocumented) export interface CustomResource extends ObjectToFetch { // (undocumented) objectType: 'customresources'; } -// @alpha (undocumented) +// @public (undocumented) export interface CustomResourcesByEntity extends KubernetesObjectsByEntity { // (undocumented) customResources: CustomResourceMatcher[]; } -// @alpha (undocumented) +// @public (undocumented) export const DEFAULT_OBJECTS: ObjectToFetch[]; -// @alpha (undocumented) +// @public (undocumented) export interface FetchResponseWrapper { // (undocumented) errors: KubernetesFetchError[]; @@ -117,10 +117,10 @@ export interface FetchResponseWrapper { responses: FetchResponse[]; } -// @alpha (undocumented) +// @public (undocumented) export interface GKEClusterDetails extends ClusterDetails {} -// @alpha (undocumented) +// @public (undocumented) export class GoogleKubernetesAuthTranslator implements KubernetesAuthTranslator { @@ -131,7 +131,7 @@ export class GoogleKubernetesAuthTranslator ): Promise<GKEClusterDetails>; } -// @alpha (undocumented) +// @public (undocumented) export class GoogleServiceAccountAuthTranslator implements KubernetesAuthTranslator { @@ -141,10 +141,10 @@ export class GoogleServiceAccountAuthTranslator ): Promise<GKEClusterDetails>; } -// @alpha +// @public export const HEADER_KUBERNETES_CLUSTER: string; -// @alpha (undocumented) +// @public (undocumented) export interface KubernetesAuthTranslator { // (undocumented) decorateClusterDetailsWithAuth( @@ -153,7 +153,7 @@ export interface KubernetesAuthTranslator { ): Promise<ClusterDetails>; } -// @alpha (undocumented) +// @public (undocumented) export class KubernetesAuthTranslatorGenerator { // (undocumented) static getKubernetesAuthTranslatorInstance( @@ -164,7 +164,7 @@ export class KubernetesAuthTranslatorGenerator { ): KubernetesAuthTranslator; } -// @alpha (undocumented) +// @public (undocumented) export class KubernetesBuilder { constructor(env: KubernetesEnvironment); // (undocumented) @@ -247,7 +247,7 @@ export class KubernetesBuilder { setServiceLocator(serviceLocator?: KubernetesServiceLocator): this; } -// @alpha +// @public export type KubernetesBuilderReturn = Promise<{ router: express.Router; clusterSupplier: KubernetesClustersSupplier; @@ -258,12 +258,12 @@ export type KubernetesBuilderReturn = Promise<{ serviceLocator: KubernetesServiceLocator; }>; -// @alpha +// @public export interface KubernetesClustersSupplier { getClusters(): Promise<ClusterDetails[]>; } -// @alpha (undocumented) +// @public (undocumented) export interface KubernetesEnvironment { // (undocumented) catalogApi: CatalogApi; @@ -273,7 +273,7 @@ export interface KubernetesEnvironment { logger: Logger; } -// @alpha +// @public export interface KubernetesFetcher { // (undocumented) fetchObjectsForService( @@ -286,7 +286,7 @@ export interface KubernetesFetcher { ): Promise<FetchResponseWrapper>; } -// @alpha (undocumented) +// @public (undocumented) export interface KubernetesObjectsByEntity { // (undocumented) auth: KubernetesRequestAuth; @@ -294,7 +294,7 @@ export interface KubernetesObjectsByEntity { entity: Entity; } -// @alpha (undocumented) +// @public (undocumented) export interface KubernetesObjectsProvider { // (undocumented) getCustomResourcesByEntity( @@ -306,7 +306,7 @@ export interface KubernetesObjectsProvider { ): Promise<ObjectsByEntityResponse>; } -// @alpha (undocumented) +// @public (undocumented) export interface KubernetesObjectsProviderOptions { // (undocumented) customResources: CustomResource[]; @@ -320,7 +320,7 @@ export interface KubernetesObjectsProviderOptions { serviceLocator: KubernetesServiceLocator; } -// @alpha (undocumented) +// @public (undocumented) export type KubernetesObjectTypes = | 'pods' | 'services' @@ -336,14 +336,14 @@ export type KubernetesObjectTypes = | 'statefulsets' | 'daemonsets'; -// @alpha +// @public export class KubernetesProxy { constructor(logger: Logger, clusterSupplier: KubernetesClustersSupplier); // (undocumented) createRequestHandler(): RequestHandler; } -// @alpha +// @public export interface KubernetesServiceLocator { // (undocumented) getClustersByEntity( @@ -354,7 +354,7 @@ export interface KubernetesServiceLocator { }>; } -// @alpha (undocumented) +// @public (undocumented) export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator { // (undocumented) decorateClusterDetailsWithAuth( @@ -362,7 +362,7 @@ export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator { ): Promise<ServiceAccountClusterDetails>; } -// @alpha (undocumented) +// @public (undocumented) export interface ObjectFetchParams { // (undocumented) clusterDetails: @@ -382,10 +382,10 @@ export interface ObjectFetchParams { serviceId: string; } -// @alpha (undocumented) +// @public (undocumented) export type ObjectsByEntityRequest = KubernetesRequestBody; -// @alpha (undocumented) +// @public (undocumented) export interface ObjectToFetch { // (undocumented) apiVersion: string; @@ -397,7 +397,7 @@ export interface ObjectToFetch { plural: string; } -// @alpha (undocumented) +// @public (undocumented) export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator { // (undocumented) decorateClusterDetailsWithAuth( @@ -406,7 +406,7 @@ export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator { ): Promise<ClusterDetails>; } -// @alpha (undocumented) +// @public (undocumented) export interface RouterOptions { // (undocumented) catalogApi: CatalogApi; @@ -420,13 +420,13 @@ export interface RouterOptions { logger: Logger; } -// @alpha (undocumented) +// @public (undocumented) export interface ServiceAccountClusterDetails extends ClusterDetails {} -// @alpha (undocumented) +// @public (undocumented) export type ServiceLocatorMethod = 'multiTenant' | 'http'; -// @alpha (undocumented) +// @public (undocumented) export interface ServiceLocatorRequestContext { // (undocumented) customResources: CustomResourceMatcher[]; @@ -434,7 +434,7 @@ export interface ServiceLocatorRequestContext { objectTypesToFetch: Set<ObjectToFetch>; } -// @alpha (undocumented) +// @public (undocumented) export type SigningCreds = { accessKeyId: string | undefined; secretAccessKey: string | undefined; diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 679162d890..e7e03ae93e 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.9.1", + "version": "0.9.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,7 +36,6 @@ "dependencies": { "@azure/identity": "^2.0.4", "@backstage/backend-common": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", @@ -45,7 +44,7 @@ "@backstage/plugin-kubernetes-common": "workspace:^", "@google-cloud/container": "^4.0.0", "@jest-mock/express": "^2.0.1", - "@kubernetes/client-node": "0.18.0", + "@kubernetes/client-node": "0.18.1", "@types/express": "^4.17.6", "@types/luxon": "^3.0.0", "aws-sdk": "^2.840.0", @@ -66,12 +65,13 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/aws4": "^1.5.1", "@types/http-proxy-middleware": "^0.19.3", "aws-sdk-mock": "^5.2.1", "mock-fs": "^5.2.0", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index 2a2432f858..81ee863cb7 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -18,39 +18,66 @@ import '@backstage/backend-common'; import { CatalogClusterLocator } from './CatalogClusterLocator'; import { CatalogApi } from '@backstage/catalog-client'; +const mockCatalogApi = { + getEntityByRef: jest.fn(), + getEntities: async () => ({ + items: [ + { + apiVersion: 'version', + kind: 'User', + metadata: { + annotations: { + 'kubernetes.io/api-server': 'https://apiserver.com', + 'kubernetes.io/api-server-certificate-authority': 'caData', + 'kubernetes.io/auth-provider': 'aws', + 'kubernetes.io/oidc-token-provider': 'google', + 'kubernetes.io/skip-metrics-lookup': 'true', + 'kubernetes.io/skip-tls-verify': 'true', + 'kubernetes.io/dashboard-url': 'my-url', + 'kubernetes.io/dashboard-app': 'my-app', + }, + name: 'owned', + namespace: 'default', + }, + }, + ], + }), +} as unknown as CatalogApi; + describe('CatalogClusterLocator', () => { - it('empty clusters returns empty cluster details', async () => { - const mockCatalogApi = { + it('returns empty cluster details when the cluster is empty', async () => { + const emptyMockCatalogApi = { getEntityByRef: jest.fn(), getEntities: async () => ({ - items: [ - { - apiVersion: 'version', - kind: 'User', - metadata: { - annotations: { - 'kubernetes.io/api-server': 'https://apiserver.com', - 'kubernetes.io/api-server-certificate-authority': 'caData', - 'kubernetes.io/auth-provider': 'aws', - }, - name: 'owned', - namespace: 'default', - }, - }, - ], + items: [], }), } as Partial<CatalogApi> as CatalogApi; - const sut = CatalogClusterLocator.fromConfig(mockCatalogApi); + const clusterSupplier = + CatalogClusterLocator.fromConfig(emptyMockCatalogApi); - const result = await sut.getClusters(); + const result = await clusterSupplier.getClusters(); + + expect(result).toHaveLength(0); + expect(result).toStrictEqual([]); + }); + + it('returns the cluster details provided by annotations', async () => { + const clusterSupplier = CatalogClusterLocator.fromConfig(mockCatalogApi); + + const result = await clusterSupplier.getClusters(); expect(result).toHaveLength(1); - expect(result[0]).toMatchObject({ + expect(result[0]).toStrictEqual({ name: 'owned', url: 'https://apiserver.com', caData: 'caData', authProvider: 'aws', + oidcTokenProvider: 'google', + skipMetricsLookup: true, + skipTLSVerify: true, + dashboardUrl: 'my-url', + dashboardApp: 'my-app', }); }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index 7183b71831..b952aefccf 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -20,7 +20,12 @@ import { ANNOTATION_KUBERNETES_API_SERVER, ANNOTATION_KUBERNETES_API_SERVER_CA, ANNOTATION_KUBERNETES_AUTH_PROVIDER, -} from '@backstage/catalog-model'; + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, + ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP, + ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY, + ANNOTATION_KUBERNETES_DASHBOARD_URL, + ANNOTATION_KUBERNETES_DASHBOARD_APP, +} from '@backstage/plugin-kubernetes-common'; export class CatalogClusterLocator implements KubernetesClustersSupplier { private catalogClient: CatalogApi; @@ -57,6 +62,26 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER_CA]!, authProvider: entity.metadata.annotations![ANNOTATION_KUBERNETES_AUTH_PROVIDER]!, + oidcTokenProvider: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER + ]!, + skipMetricsLookup: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP + ]! === 'true' + ? true + : false, + skipTLSVerify: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY + ]! === 'true' + ? true + : false, + dashboardUrl: + entity.metadata.annotations![ANNOTATION_KUBERNETES_DASHBOARD_URL]!, + dashboardApp: + entity.metadata.annotations![ANNOTATION_KUBERNETES_DASHBOARD_APP]!, }; return clusterDetails; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index ccb0f28109..b89945902d 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -20,7 +20,7 @@ import { KubernetesAuthTranslator } from './types'; /** * - * @alpha + * @public */ export type SigningCreds = { accessKeyId: string | undefined; @@ -30,7 +30,7 @@ export type SigningCreds = { /** * - * @alpha + * @public */ export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts index 6b1f2281c4..2633598d6c 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts @@ -27,7 +27,7 @@ const aksScope = '6dae42f8-4368-4678-94ff-3960e28e3630/.default'; // This scope /** * - * @alpha + * @public */ export class AzureIdentityKubernetesAuthTranslator implements KubernetesAuthTranslator diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts index cabf66b40b..e8dae56997 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts @@ -20,7 +20,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** * - * @alpha + * @public */ export class GoogleKubernetesAuthTranslator implements KubernetesAuthTranslator diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts index 1eca62b363..cca510ad6c 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts @@ -19,7 +19,7 @@ import * as container from '@google-cloud/container'; /** * - * @alpha + * @public */ export class GoogleServiceAccountAuthTranslator implements KubernetesAuthTranslator diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts index d4eed29653..f54e46efab 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts @@ -25,7 +25,7 @@ import { OidcKubernetesAuthTranslator } from './OidcKubernetesAuthTranslator'; /** * - * @alpha + * @public */ export class KubernetesAuthTranslatorGenerator { static getKubernetesAuthTranslatorInstance( diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts index 2ef78495a1..32aa141e50 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts @@ -19,7 +19,7 @@ import { ServiceAccountClusterDetails } from '../types/types'; /** * - * @alpha + * @public */ export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator { async decorateClusterDetailsWithAuth( diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts index e5450b2fe1..cee1ec1e0c 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts @@ -20,7 +20,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** * - * @alpha + * @public */ export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator { async decorateClusterDetailsWithAuth( diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts index f8417f8468..efbbff8250 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts @@ -19,7 +19,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** * - * @alpha + * @public */ export interface KubernetesAuthTranslator { decorateClusterDetailsWithAuth( diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 2766f9abfc..cc2dea26c0 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -31,6 +31,10 @@ import { import { KubernetesBuilder } from './KubernetesBuilder'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { CatalogApi } from '@backstage/catalog-client'; +import { HEADER_KUBERNETES_CLUSTER } from './KubernetesProxy'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { rest } from 'msw'; describe('KubernetesBuilder', () => { let app: express.Express; @@ -267,4 +271,65 @@ describe('KubernetesBuilder', () => { expect(response.status).toEqual(200); }); }); + + describe('post /proxy', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + beforeEach(() => { + worker.use( + rest.post('https://localhost:1234/api/v1/namespaces', (req, res, ctx) => + req + .arrayBuffer() + .then(body => + res( + ctx.set('content-type', `${req.headers.get('content-type')}`), + ctx.body(body), + ), + ), + ), + ); + }); + + it('returns the given request body', async () => { + const requestBody = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + + const proxyEndpointRequest = request(app) + .post('/proxy/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .send(requestBody); + + worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); + + const response = await proxyEndpointRequest; + + expect(response.body).toStrictEqual(requestBody); + }); + + it('supports yaml content type', async () => { + const requestBody = `--- +kind: Namespace +apiVersion: v1 +metadata: + name: new-ns +`; + + const proxyEndpointRequest = request(app) + .post('/proxy/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .set('content-type', 'application/yaml') + .send(requestBody); + + worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); + + const response = await proxyEndpointRequest; + expect(response.text).toEqual(requestBody); + }); + }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 1e7b704ec5..74ec9eada2 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -43,7 +43,7 @@ import { KubernetesProxy } from './KubernetesProxy'; /** * - * @alpha + * @public */ export interface KubernetesEnvironment { logger: Logger; @@ -54,7 +54,7 @@ export interface KubernetesEnvironment { /** * The return type of the `KubernetesBuilder.build` method * - * @alpha + * @public */ export type KubernetesBuilderReturn = Promise<{ router: express.Router; @@ -68,7 +68,7 @@ export type KubernetesBuilderReturn = Promise<{ /** * - * @alpha + * @public */ export class KubernetesBuilder { private clusterSupplier?: KubernetesClustersSupplier; @@ -265,6 +265,7 @@ export class KubernetesBuilder { ): express.Router { const logger = this.env.logger; const router = Router(); + router.use('/proxy', proxy.createRequestHandler()); router.use(express.json()); // @deprecated @@ -297,8 +298,6 @@ export class KubernetesBuilder { }); }); - router.use('/proxy', proxy.createRequestHandler()); - addResourceRoutesToRouter(router, catalogApi, objectsProvider); return router; diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index a738c69bcc..63e46a44e3 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -51,7 +51,7 @@ import { /** * - * @alpha + * @public */ export const DEFAULT_OBJECTS: ObjectToFetch[] = [ { diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 9d88d2ab84..178105ef79 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -32,14 +32,14 @@ export const APPLICATION_JSON: string = 'application/json'; /** * The header that is used to specify the cluster name. * - * @alpha + * @public */ export const HEADER_KUBERNETES_CLUSTER: string = 'X-Kubernetes-Cluster'; /** * A proxy that routes requests to the Kubernetes API. * - * @alpha + * @public */ export class KubernetesProxy { private readonly middlewareForClusterName = new Map<string, RequestHandler>(); diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index ab27a75a18..341956553e 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -24,7 +24,7 @@ import { CatalogApi } from '@backstage/catalog-client'; /** * - * @alpha + * @public */ export interface RouterOptions { logger: Logger; @@ -47,7 +47,7 @@ export interface RouterOptions { * }).build(); * ``` * - * @alpha + * @public */ export async function createRouter( options: RouterOptions, diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index d76bfc8149..25c7abfd1f 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -28,7 +28,7 @@ import type { /** * - * @alpha + * @public */ export interface ObjectFetchParams { serviceId: string; @@ -46,7 +46,7 @@ export interface ObjectFetchParams { /** * Fetches information from a kubernetes cluster using the cluster details object to target a specific cluster * - * @alpha + * @public */ export interface KubernetesFetcher { fetchObjectsForService( @@ -60,7 +60,7 @@ export interface KubernetesFetcher { /** * - * @alpha + * @public */ export interface FetchResponseWrapper { errors: KubernetesFetchError[]; @@ -69,7 +69,7 @@ export interface FetchResponseWrapper { /** * - * @alpha + * @public */ export interface ObjectToFetch { objectType: KubernetesObjectTypes; @@ -80,7 +80,7 @@ export interface ObjectToFetch { /** * - * @alpha + * @public */ export interface CustomResource extends ObjectToFetch { objectType: 'customresources'; @@ -88,7 +88,7 @@ export interface CustomResource extends ObjectToFetch { /** * - * @alpha + * @public */ export type KubernetesObjectTypes = | 'pods' @@ -107,7 +107,7 @@ export type KubernetesObjectTypes = /** * Used to load cluster details from different sources - * @alpha + * @public */ export interface KubernetesClustersSupplier { /** @@ -120,7 +120,7 @@ export interface KubernetesClustersSupplier { } /** - * @alpha + * @public */ export interface ServiceLocatorRequestContext { objectTypesToFetch: Set<ObjectToFetch>; @@ -129,7 +129,7 @@ export interface ServiceLocatorRequestContext { /** * Used to locate which cluster(s) a service is running on - * @alpha + * @public */ export interface KubernetesServiceLocator { getClustersByEntity( @@ -140,13 +140,13 @@ export interface KubernetesServiceLocator { /** * - * @alpha + * @public */ export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http /** * - * @alpha + * @public */ export interface ClusterDetails { /** @@ -209,25 +209,25 @@ export interface ClusterDetails { /** * - * @alpha + * @public */ export interface GKEClusterDetails extends ClusterDetails {} /** * - * @alpha + * @public */ export interface AzureClusterDetails extends ClusterDetails {} /** * - * @alpha + * @public */ export interface ServiceAccountClusterDetails extends ClusterDetails {} /** * - * @alpha + * @public */ export interface AWSClusterDetails extends ClusterDetails { assumeRole?: string; @@ -236,7 +236,7 @@ export interface AWSClusterDetails extends ClusterDetails { /** * - * @alpha + * @public */ export interface KubernetesObjectsProviderOptions { logger: Logger; @@ -248,13 +248,13 @@ export interface KubernetesObjectsProviderOptions { /** * - * @alpha + * @public */ export type ObjectsByEntityRequest = KubernetesRequestBody; /** * - * @alpha + * @public */ export interface KubernetesObjectsByEntity { entity: Entity; @@ -263,7 +263,7 @@ export interface KubernetesObjectsByEntity { /** * - * @alpha + * @public */ export interface CustomResourcesByEntity extends KubernetesObjectsByEntity { customResources: CustomResourceMatcher[]; @@ -271,7 +271,7 @@ export interface CustomResourcesByEntity extends KubernetesObjectsByEntity { /** * - * @alpha + * @public */ export interface KubernetesObjectsProvider { getKubernetesObjectsByEntity( diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 26e2cb3c47..281d2a83af 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-kubernetes-common +## 0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + +## 0.6.0 + +### Minor Changes + +- 1728c1ef01: **BREAKING**: Renamed misspelled `LimitRangeFetchReponse` to `LimitRangeFetchResponse`. + +### Patch Changes + +- 2518ef5b8a: New K8s catalog entity annotations added that will replace now deprecated k8s annotations in the catalog-model package. K8s annotation imports should now be made from plugin-kubernetes-common. +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/catalog-model@1.2.0 + +## 0.6.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + +## 0.6.0-next.1 + +### Patch Changes + +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + +## 0.6.0-next.0 + +### Minor Changes + +- 1728c1ef01: **BREAKING**: Renamed misspelled `LimitRangeFetchReponse` to `LimitRangeFetchResponse`. + +### Patch Changes + +- 2518ef5b8a: New K8s catalog entity annotations added that will replace now deprecated k8s annotations in the catalog-model package. K8s annotation imports should now be made from plugin-kubernetes-common. +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + ## 0.5.1 ### Patch Changes diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index fe8aa19966..fb41895f1a 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -19,6 +19,37 @@ import { V1ReplicaSet } from '@kubernetes/client-node'; import { V1Service } from '@kubernetes/client-node'; import { V1StatefulSet } from '@kubernetes/client-node'; +// @public +export const ANNOTATION_KUBERNETES_API_SERVER = 'kubernetes.io/api-server'; + +// @public +export const ANNOTATION_KUBERNETES_API_SERVER_CA = + 'kubernetes.io/api-server-certificate-authority'; + +// @public +export const ANNOTATION_KUBERNETES_AUTH_PROVIDER = + 'kubernetes.io/auth-provider'; + +// @public +export const ANNOTATION_KUBERNETES_DASHBOARD_APP = + 'kubernetes.io/dashboard-app'; + +// @public +export const ANNOTATION_KUBERNETES_DASHBOARD_URL = + 'kubernetes.io/dashboard-url'; + +// @public +export const ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER = + 'kubernetes.io/oidc-token-provider'; + +// @public +export const ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP = + 'kubernetes.io/skip-metrics-lookup'; + +// @public +export const ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY = + 'kubernetes.io/skip-tls-verify'; + // @public (undocumented) export type AuthProviderType = 'google' | 'serviceAccount' | 'aws' | 'azure'; @@ -140,7 +171,7 @@ export type FetchResponse = | ServiceFetchResponse | ConfigMapFetchResponse | DeploymentFetchResponse - | LimitRangeFetchReponse + | LimitRangeFetchResponse | ReplicaSetsFetchResponse | HorizontalPodAutoscalersFetchResponse | JobsFetchResponse @@ -205,7 +236,7 @@ export interface KubernetesRequestBody { } // @public (undocumented) -export interface LimitRangeFetchReponse { +export interface LimitRangeFetchResponse { // (undocumented) resources: Array<V1LimitRange>; // (undocumented) diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index faae062de2..30c2e9befd 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.5.1", + "version": "0.6.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,7 +39,7 @@ }, "dependencies": { "@backstage/catalog-model": "workspace:^", - "@kubernetes/client-node": "0.18.0" + "@kubernetes/client-node": "0.18.1" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/plugins/kubernetes-common/src/catalog-entity-constants.ts b/plugins/kubernetes-common/src/catalog-entity-constants.ts new file mode 100644 index 0000000000..a1a1996777 --- /dev/null +++ b/plugins/kubernetes-common/src/catalog-entity-constants.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Annotation for specifying the API server of a Kubernetes cluster + * + * @public + */ +export const ANNOTATION_KUBERNETES_API_SERVER = 'kubernetes.io/api-server'; + +/** + * Annotation for specifying the Certificate Authority of an API server for a Kubernetes cluster + * + * @public + */ +export const ANNOTATION_KUBERNETES_API_SERVER_CA = + 'kubernetes.io/api-server-certificate-authority'; + +/** + * Annotation for specifying the auth provider for a Kubernetes cluster + * + * @public + */ +export const ANNOTATION_KUBERNETES_AUTH_PROVIDER = + 'kubernetes.io/auth-provider'; + +/** + * Annotation for specifying the oidc provider used to get id tokens for a Kubernetes cluster + * + * @public + */ +export const ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER = + 'kubernetes.io/oidc-token-provider'; + +/** + * Annotation for specifying boolean value for skip metric lookup. + * + * @public + */ +export const ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP = + 'kubernetes.io/skip-metrics-lookup'; + +/** + * Annotation for specifying boolean value for skip tls verify. + * + * @public + */ +export const ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY = + 'kubernetes.io/skip-tls-verify'; + +/** + * Annotation for specifying the dashboard url for a Kubernetes cluster. + * + * @public + */ +export const ANNOTATION_KUBERNETES_DASHBOARD_URL = + 'kubernetes.io/dashboard-url'; + +/** + * Annotation for specifying the dashboard app for a Kubernetes cluster. + * + * @public + */ +export const ANNOTATION_KUBERNETES_DASHBOARD_APP = + 'kubernetes.io/dashboard-app'; diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index 6b97c0eb7d..ade73e4f79 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -21,3 +21,4 @@ */ export * from './types'; +export * from './catalog-entity-constants'; diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index f723a46d16..f5701d49fe 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -127,7 +127,7 @@ export type FetchResponse = | ServiceFetchResponse | ConfigMapFetchResponse | DeploymentFetchResponse - | LimitRangeFetchReponse + | LimitRangeFetchResponse | ReplicaSetsFetchResponse | HorizontalPodAutoscalersFetchResponse | JobsFetchResponse @@ -169,7 +169,7 @@ export interface ReplicaSetsFetchResponse { } /** @public */ -export interface LimitRangeFetchReponse { +export interface LimitRangeFetchResponse { type: 'limitranges'; resources: Array<V1LimitRange>; } diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 1abf979827..a6f5a90e65 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-kubernetes +## 0.7.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-kubernetes-common@0.6.1-next.0 + +## 0.7.8 + +### Patch Changes + +- 145a79a15b: Condenses kubernetes ui plugin to fit more onscreen and increase visibility +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.0 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + +## 0.7.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-kubernetes-common@0.6.0-next.2 + +## 0.7.8-next.1 + +### Patch Changes + +- 145a79a15b: Condenses kubernetes ui plugin to fit more onscreen and increase visibility +- 628e2bd89a: Updated dependency `@kubernetes/client-node` to `0.18.1`. +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.6.0-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.7.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.7.7 ### Patch Changes diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 463b4126eb..2cc2674a9e 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -101,7 +101,7 @@ export interface DeploymentResources { replicaSets: V1ReplicaSet[]; } -// @alpha +// @public export interface DetectedError { // (undocumented) cluster: string; @@ -117,10 +117,10 @@ export interface DetectedError { severity: ErrorSeverity; } -// @alpha +// @public export type DetectedErrorsByCluster = Map<string, DetectedError[]>; -// @alpha +// @public export const detectErrors: ( objects: ObjectsByEntityResponse, ) => DetectedErrorsByCluster; @@ -137,7 +137,7 @@ export type EntityKubernetesContentProps = { refreshIntervalMs?: number; }; -// @alpha +// @public export type ErrorDetectableKind = | 'Pod' | 'Deployment' @@ -161,7 +161,7 @@ export const ErrorReporting: ({ detectedErrors, }: ErrorReportingProps) => JSX.Element; -// @alpha +// @public export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; // Warning: (ae-forgotten-export) The symbol "FormatClusterLinkOptions" needs to be exported by the entry point index.d.ts diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 8edbc3ae15..b69efb0d65 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.7.7", + "version": "0.7.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -40,7 +40,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/theme": "workspace:^", - "@kubernetes/client-node": "0.18.0", + "@kubernetes/client-node": "0.18.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -66,7 +66,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/kubernetes/src/components/Cluster/Cluster.tsx b/plugins/kubernetes/src/components/Cluster/Cluster.tsx index 8998c68253..58025955b2 100644 --- a/plugins/kubernetes/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes/src/components/Cluster/Cluster.tsx @@ -19,7 +19,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Divider, Grid, Typography, } from '@material-ui/core'; @@ -60,11 +59,12 @@ const ClusterSummary = ({ <Grid container direction="row" - justifyContent="flex-start" + justifyContent="space-between" alignItems="flex-start" + spacing={0} > <Grid - xs={4} + xs={6} item container direction="column" @@ -73,22 +73,20 @@ const ClusterSummary = ({ spacing={0} > <Grid item xs> - <Typography variant="h3">{clusterName}</Typography> - <Typography color="textSecondary" variant="body1"> + <Typography variant="body1">{clusterName}</Typography> + <Typography color="textSecondary" variant="subtitle1"> Cluster </Typography> </Grid> </Grid> - <Grid item xs={1}> - <Divider style={{ height: '4em' }} orientation="vertical" /> - </Grid> <Grid item container xs={3} direction="column" justifyContent="flex-start" - alignItems="flex-start" + alignItems="flex-end" + spacing={0} > <Grid item> <StatusOK>{totalNumberOfPods} pods</StatusOK> @@ -137,24 +135,36 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { </AccordionSummary> <AccordionDetails> <Grid container direction="column"> - <Grid item> - <CustomResources /> - </Grid> - <Grid item> - <DeploymentsAccordions /> - </Grid> - <Grid item> - <StatefulSetsAccordions /> - </Grid> - <Grid item> - <IngressesAccordions /> - </Grid> - <Grid item> - <ServicesAccordions /> - </Grid> - <Grid item> - <CronJobsAccordions /> - </Grid> + {groupedResponses.customResources.length > 0 ? ( + <Grid item> + <CustomResources /> + </Grid> + ) : undefined} + {groupedResponses.deployments.length > 0 ? ( + <Grid item> + <DeploymentsAccordions /> + </Grid> + ) : undefined} + {groupedResponses.statefulsets.length > 0 ? ( + <Grid item> + <StatefulSetsAccordions /> + </Grid> + ) : undefined} + {groupedResponses.ingresses.length > 0 ? ( + <Grid item> + <IngressesAccordions /> + </Grid> + ) : undefined} + {groupedResponses.services.length > 0 ? ( + <Grid item> + <ServicesAccordions /> + </Grid> + ) : undefined} + {groupedResponses.cronJobs.length > 0 ? ( + <Grid item> + <CronJobsAccordions /> + </Grid> + ) : undefined} </Grid> </AccordionDetails> </Accordion> diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx index ce2dc340e7..c5babe67fc 100644 --- a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx @@ -18,7 +18,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Divider, Grid, Typography, } from '@material-ui/core'; @@ -51,22 +50,21 @@ const CronJobSummary = ({ cronJob }: CronJobSummaryProps) => { <Grid container direction="row" - justifyContent="flex-start" + justifyContent="space-between" alignItems="center" + spacing={0} > - <Grid xs={3} item> + <Grid xs={6} item> <CronJobDrawer cronJob={cronJob} /> </Grid> - <Grid item xs={1}> - <Divider style={{ height: '5em' }} orientation="vertical" /> - </Grid> <Grid item container - xs={5} + xs={6} direction="column" justifyContent="flex-start" - alignItems="flex-start" + alignItems="flex-end" + spacing={0} > <Grid item> {cronJob.spec?.suspend ? ( @@ -92,7 +90,7 @@ const CronJobSummary = ({ cronJob }: CronJobSummaryProps) => { const CronJobAccordion = ({ cronJob, ownedJobs }: CronJobAccordionProps) => { return ( - <Accordion TransitionProps={{ unmountOnExit: true }}> + <Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined"> <AccordionSummary expandIcon={<ExpandMoreIcon />}> <CronJobSummary cronJob={cronJob} /> </AccordionSummary> diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx index 758f17ade0..0dc53396bb 100644 --- a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx @@ -47,12 +47,12 @@ export const CronJobDrawer = ({ spacing={0} > <Grid item> - <Typography variant="h5"> + <Typography variant="body1"> {cronJob.metadata?.name ?? 'unknown object'} </Typography> </Grid> <Grid item> - <Typography color="textSecondary" variant="body1"> + <Typography color="textSecondary" variant="subtitle1"> CronJob </Typography> </Grid> diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx index 185eeb771e..91039ec314 100644 --- a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx @@ -19,7 +19,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Divider, Grid, Typography, } from '@material-ui/core'; @@ -101,15 +100,13 @@ const RolloutSummary = ({ <Grid container direction="row" - justifyContent="flex-start" + justifyContent="space-between" alignItems="center" + spacing={0} > - <Grid xs={3} item> + <Grid xs={6} item> <RolloutDrawer rollout={rollout} /> </Grid> - <Grid item xs={1}> - <Divider style={{ height: '5em' }} orientation="vertical" /> - </Grid> {hpa && ( <Grid item xs={3}> <HorizontalPodAutoscalerDrawer hpa={hpa}> @@ -149,7 +146,8 @@ const RolloutSummary = ({ xs={3} direction="column" justifyContent="flex-start" - alignItems="flex-start" + alignItems="flex-end" + spacing={0} > <Grid item> <StatusOK>{numberOfCurrentPods} pods</StatusOK> @@ -210,6 +208,7 @@ const RolloutAccordion = ({ <Accordion defaultExpanded={defaultExpanded} TransitionProps={{ unmountOnExit: true }} + variant="outlined" > <AccordionSummary expandIcon={<ExpandMoreIcon />}> <RolloutSummary diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx index d719f707d1..f1c4fe7f7e 100644 --- a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx @@ -40,12 +40,12 @@ export const RolloutDrawer = ({ spacing={0} > <Grid item> - <Typography variant="h5"> + <Typography variant="body1"> {rollout.metadata?.name ?? 'unknown object'} </Typography> </Grid> <Grid item> - <Typography color="textSecondary" variant="body1"> + <Typography color="textSecondary" variant="subtitle1"> Rollout </Typography> </Grid> diff --git a/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.tsx b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.tsx index 36d38d3fe0..cee3fb9e6c 100644 --- a/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.tsx +++ b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.tsx @@ -19,7 +19,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Divider, Grid, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; @@ -54,18 +53,16 @@ const DefaultCustomResourceSummary = ({ <Grid container direction="row" - justifyContent="flex-start" + justifyContent="space-between" alignItems="center" + spacing={0} > - <Grid xs={3} item> + <Grid xs={12} item> <DefaultCustomResourceDrawer customResource={customResource} customResourceName={customResourceName} /> </Grid> - <Grid item xs={1}> - <Divider style={{ height: '5em' }} orientation="vertical" /> - </Grid> </Grid> ); }; @@ -79,6 +76,7 @@ const DefaultCustomResourceAccordion = ({ <Accordion defaultExpanded={defaultExpanded} TransitionProps={{ unmountOnExit: true }} + variant="outlined" > <AccordionSummary expandIcon={<ExpandMoreIcon />}> <DefaultCustomResourceSummary diff --git a/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx index ba6169cdf3..5b968f72a5 100644 --- a/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx +++ b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx @@ -47,12 +47,12 @@ export const DefaultCustomResourceDrawer = ({ spacing={0} > <Grid item> - <Typography variant="h5"> + <Typography variant="body1"> {customResource.metadata?.name ?? 'unknown object'} </Typography> </Grid> <Grid item> - <Typography color="textSecondary" variant="body1"> + <Typography color="textSecondary" variant="subtitle1"> {capitalizedName} </Typography> </Grid> diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx index 6d25702d70..8be5ee1e46 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx @@ -58,12 +58,12 @@ export const DeploymentDrawer = ({ spacing={0} > <Grid item> - <Typography variant="h5"> + <Typography variant="body1"> {deployment.metadata?.name ?? 'unknown object'} </Typography> </Grid> <Grid item> - <Typography color="textSecondary" variant="body1"> + <Typography color="textSecondary" variant="subtitle1"> Deployment </Typography> </Grid> diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx index d2fb6275bc..8432b9d133 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx @@ -19,7 +19,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Divider, Grid, Typography, } from '@material-ui/core'; @@ -72,17 +71,15 @@ const DeploymentSummary = ({ <Grid container direction="row" - justifyContent="flex-start" + justifyContent="space-between" alignItems="center" + spacing={0} > - <Grid xs={3} item> + <Grid xs={4} item> <DeploymentDrawer deployment={deployment} /> </Grid> - <Grid item xs={1}> - <Divider style={{ height: '5em' }} orientation="vertical" /> - </Grid> {hpa && ( - <Grid item xs={3}> + <Grid item xs={4}> <HorizontalPodAutoscalerDrawer hpa={hpa}> <Grid item @@ -117,10 +114,11 @@ const DeploymentSummary = ({ <Grid item container - xs={3} + xs={4} direction="column" justifyContent="flex-start" - alignItems="flex-start" + alignItems="flex-end" + spacing={0} > <Grid item> <StatusOK>{numberOfCurrentPods} pods</StatusOK> @@ -152,7 +150,7 @@ const DeploymentAccordion = ({ ); return ( - <Accordion TransitionProps={{ unmountOnExit: true }}> + <Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined"> <AccordionSummary expandIcon={<ExpandMoreIcon />}> <DeploymentSummary deployment={deployment} diff --git a/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx b/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx index ed51b7d21d..a99fc21902 100644 --- a/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx +++ b/plugins/kubernetes/src/components/ErrorReporting/ErrorReporting.tsx @@ -102,7 +102,7 @@ export const ErrorReporting = ({ detectedErrors }: ErrorReportingProps) => { title="Error Reporting" data={errors} columns={columns} - options={{ paging: true, search: false }} + options={{ paging: true, search: false, emptyRowsWhenPaging: false }} /> )} </> diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx index dc88987d53..ea79c1e4dc 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx @@ -43,12 +43,12 @@ export const IngressDrawer = ({ spacing={0} > <Grid item> - <Typography variant="h5"> + <Typography variant="body1"> {ingress.metadata?.name ?? 'unknown object'} </Typography> </Grid> <Grid item> - <Typography color="textSecondary" variant="body1"> + <Typography color="textSecondary" variant="subtitle1"> Ingress </Typography> </Grid> diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx index f6d6e4bf68..6e12c033ba 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx @@ -19,7 +19,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Divider, Grid, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; @@ -46,13 +45,9 @@ const IngressSummary = ({ ingress }: IngressSummaryProps) => { justifyContent="flex-start" alignItems="center" > - <Grid xs={3} item> + <Grid xs={12} item> <IngressDrawer ingress={ingress} /> </Grid> - - <Grid item xs={1}> - <Divider style={{ height: '5em' }} orientation="vertical" /> - </Grid> </Grid> ); }; diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx index 715be4c88a..e805e3664b 100644 --- a/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx @@ -18,7 +18,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Divider, Grid, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; @@ -54,22 +53,21 @@ const JobSummary = ({ job }: JobSummaryProps) => { <Grid container direction="row" - justifyContent="flex-start" + justifyContent="space-between" alignItems="center" + spacing={0} > - <Grid xs={3} item> + <Grid xs={6} item> <JobDrawer job={job} /> </Grid> - <Grid item xs={1}> - <Divider style={{ height: '5em' }} orientation="vertical" /> - </Grid> <Grid item container - xs={8} + xs={6} direction="column" justifyContent="flex-start" - alignItems="flex-start" + alignItems="flex-end" + spacing={0} > <Grid item> {job.status?.succeeded && <StatusOK>Succeeded</StatusOK>} @@ -89,7 +87,7 @@ const JobSummary = ({ job }: JobSummaryProps) => { const JobAccordion = ({ job, ownedPods }: JobAccordionProps) => { return ( - <Accordion TransitionProps={{ unmountOnExit: true }}> + <Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined"> <AccordionSummary expandIcon={<ExpandMoreIcon />}> <JobSummary job={job} /> </AccordionSummary> diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx index a7217725b2..b91fb15541 100644 --- a/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx @@ -47,12 +47,12 @@ export const JobDrawer = ({ spacing={0} > <Grid item> - <Typography variant="h6"> + <Typography variant="body1"> {job.metadata?.name ?? 'unknown object'} </Typography> </Grid> <Grid item> - <Typography color="textSecondary" variant="body1"> + <Typography color="textSecondary" variant="subtitle1"> Job </Typography> </Grid> diff --git a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx index 8a177cf6dd..7e194c3ec2 100644 --- a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx +++ b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx @@ -33,7 +33,7 @@ import { V1ObjectMeta } from '@kubernetes/client-node'; import { withStyles } from '@material-ui/core/styles'; import jsYaml from 'js-yaml'; import { - Button as BackstageButton, + LinkButton as BackstageButton, CodeSnippet, StructuredMetadataTable, WarningPanel, diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.tsx index 1d2369e322..a6e4b33020 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.tsx @@ -46,6 +46,7 @@ const DEFAULT_COLUMNS: TableColumn<V1Pod>[] = [ { title: 'phase', render: (pod: V1Pod) => pod.status?.phase ?? 'unknown', + width: 'auto', }, { title: 'status', @@ -58,12 +59,14 @@ const READY: TableColumn<V1Pod>[] = [ title: 'containers ready', align: 'center', render: containersReady, + width: 'auto', }, { title: 'total restarts', align: 'center', render: totalRestarts, type: 'numeric', + width: 'auto', }, ]; @@ -87,6 +90,7 @@ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => { return podStatusToCpuUtil(metrics); }, + width: 'auto', }, { title: 'Memory usage %', @@ -99,6 +103,7 @@ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => { return podStatusToMemoryUtil(metrics); }, + width: 'auto', }, ]; columns.push(...resourceColumns); @@ -109,11 +114,13 @@ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => { width: '100%', }; + const usePods = pods.map(p => ({ ...p, id: p.metadata?.uid })); + return ( <div style={tableStyle}> <Table - options={{ paging: true, search: false }} - data={pods} + options={{ paging: true, search: false, emptyRowsWhenPaging: false }} + data={usePods} columns={columns} /> </div> diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx index 5d17fd7213..a088c4792d 100644 --- a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx @@ -43,12 +43,12 @@ export const ServiceDrawer = ({ spacing={0} > <Grid item> - <Typography variant="h5"> + <Typography variant="body1"> {service.metadata?.name ?? 'unknown object'} </Typography> </Grid> <Grid item> - <Typography color="textSecondary" variant="body1"> + <Typography color="textSecondary" variant="subtitle1"> Service </Typography> </Grid> diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx index a51b93b3bc..f0cb99ec9c 100644 --- a/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx @@ -19,7 +19,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Divider, Grid, Typography, } from '@material-ui/core'; @@ -38,17 +37,14 @@ const ServiceSummary = ({ service }: ServiceSummaryProps) => { <Grid container direction="row" - justifyContent="flex-start" + justifyContent="space-between" alignItems="center" + spacing={0} > - <Grid xs={3} item> + <Grid xs={8} item> <ServiceDrawer service={service} /> </Grid> - <Grid item xs={1}> - <Divider style={{ height: '5em' }} orientation="vertical" /> - </Grid> - <Grid item> <Typography variant="subtitle2"> Type: {service.spec?.type ?? '?'} @@ -95,7 +91,7 @@ type ServiceAccordionProps = { const ServiceAccordion = ({ service }: ServiceAccordionProps) => { return ( - <Accordion TransitionProps={{ unmountOnExit: true }}> + <Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined"> <AccordionSummary expandIcon={<ExpandMoreIcon />}> <ServiceSummary service={service} /> </AccordionSummary> diff --git a/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx b/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx index 5dd3bafe37..e83c890426 100644 --- a/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx +++ b/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx @@ -59,12 +59,12 @@ export const StatefulSetDrawer = ({ spacing={0} > <Grid item> - <Typography variant="h5"> + <Typography variant="body1"> {statefulset.metadata?.name ?? 'unknown object'} </Typography> </Grid> <Grid item> - <Typography color="textSecondary" variant="body1"> + <Typography color="textSecondary" variant="subtitle1"> Stateful Set </Typography> </Grid> diff --git a/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetsAccordions.tsx b/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetsAccordions.tsx index bb1c4821ba..37bb76c84e 100644 --- a/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetsAccordions.tsx +++ b/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetsAccordions.tsx @@ -19,7 +19,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Divider, Grid, Typography, } from '@material-ui/core'; @@ -69,15 +68,13 @@ const StatefulSetSummary = ({ <Grid container direction="row" - justifyContent="flex-start" + justifyContent="space-between" alignItems="center" + spacing={0} > - <Grid xs={3} item> + <Grid xs={6} item> <StatefulSetDrawer statefulset={statefulset} /> </Grid> - <Grid item xs={1}> - <Divider style={{ height: '5em' }} orientation="vertical" /> - </Grid> {hpa && ( <Grid item xs={3}> <HorizontalPodAutoscalerDrawer hpa={hpa}> @@ -118,6 +115,7 @@ const StatefulSetSummary = ({ direction="column" justifyContent="flex-start" alignItems="flex-start" + spacing={0} > <Grid item> <StatusOK>{numberOfCurrentPods} pods</StatusOK> @@ -149,7 +147,7 @@ const StatefulSetAccordion = ({ ); return ( - <Accordion TransitionProps={{ unmountOnExit: true }}> + <Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined"> <AccordionSummary expandIcon={<ExpandMoreIcon />}> <StatefulSetSummary statefulset={statefulset} diff --git a/plugins/kubernetes/src/error-detection/error-detection.ts b/plugins/kubernetes/src/error-detection/error-detection.ts index 3a9f1571e0..fddd4d838a 100644 --- a/plugins/kubernetes/src/error-detection/error-detection.ts +++ b/plugins/kubernetes/src/error-detection/error-detection.ts @@ -25,7 +25,7 @@ import { detectErrorsInHpa } from './hpas'; * For each cluster try to find errors in each of the object types provided * returning a map of cluster names to errors in that cluster * - * @alpha + * @public */ export const detectErrors = ( objects: ObjectsByEntityResponse, diff --git a/plugins/kubernetes/src/error-detection/types.ts b/plugins/kubernetes/src/error-detection/types.ts index 5e35cef8b6..4bc7ae164d 100644 --- a/plugins/kubernetes/src/error-detection/types.ts +++ b/plugins/kubernetes/src/error-detection/types.ts @@ -24,7 +24,7 @@ import { /** * Severity of the error, where 10 is critical and 0 is very low. * - * @alpha + * @public */ export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; @@ -33,7 +33,7 @@ export type ErrorDetectable = V1Pod | V1Deployment | V1HorizontalPodAutoscaler; /** * Kubernetes kinds that errors might be reported by the plugin * - * @alpha + * @public */ export type ErrorDetectableKind = | 'Pod' @@ -43,14 +43,14 @@ export type ErrorDetectableKind = /** * A list of errors keyed by Cluster name * - * @alpha + * @public */ export type DetectedErrorsByCluster = Map<string, DetectedError[]>; /** * Represents an error found on a Kubernetes object * - * @alpha + * @public */ export interface DetectedError { severity: ErrorSeverity; diff --git a/plugins/lighthouse-backend/.eslintrc.js b/plugins/lighthouse-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/lighthouse-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/lighthouse-backend/CHANGELOG.md b/plugins/lighthouse-backend/CHANGELOG.md new file mode 100644 index 0000000000..a7a8b26203 --- /dev/null +++ b/plugins/lighthouse-backend/CHANGELOG.md @@ -0,0 +1,48 @@ +# @backstage/plugin-lighthouse-backend + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-lighthouse-common@0.1.0 + +## 0.1.0 + +### Minor Changes + +- eef62546ce: Introduce Lighthouse Backend Plugin to run scheduled Lighthouse Audits + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-lighthouse-common@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.1.0-next.0 + +### Minor Changes + +- eef62546ce: Introduce Lighthouse Backend Plugin to run scheduled Lighthouse Audits + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-common@0.1.0-next.0 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 diff --git a/plugins/lighthouse-backend/README.md b/plugins/lighthouse-backend/README.md new file mode 100644 index 0000000000..2c648754b3 --- /dev/null +++ b/plugins/lighthouse-backend/README.md @@ -0,0 +1,69 @@ +# Lighthouse Backend + +Lighthouse Backend allows you to run scheduled lighthouse Tests for each Website with the annotation `lighthouse.com/website-url`. + +## Setup + +1. Install the plugin using: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-lighthouse-backend +``` + +2. Create a `lighthouse.ts` file inside `packages/backend/src/plugins/`: + +```typescript +import { createScheduler } from '@backstage/plugin-lighthouse-backend'; +import { PluginEnvironment } from '../types'; +import { CatalogClient } from '@backstage/catalog-client'; + +export default async function createPlugin(env: PluginEnvironment) { + const { logger, scheduler, config } = env; + + const catalogClient = new CatalogClient({ + discoveryApi: env.discovery, + }); + + await createScheduler({ logger, scheduler, config, catalogClient }); +} +``` + +3. Modify your `packages/backend/src/index.ts` to include: + +```diff + ... + + import { Config } from '@backstage/config'; + import app from './plugins/app'; ++import lighthouse from './plugins/lighthouse'; + import scaffolder from './plugins/scaffolder'; + + ... + + async function main() { + + ... + + const authEnv = useHotMemoize(module, () => createEnv('auth')); ++ const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse')); + const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); + + ... + + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); + ++ await lighthouse(lighthouseEnv) +``` + +## Configuration + +You can define how often and when the scheduler should run the audits: + +```yaml +lighthouse: + schedule: + days: 1 +``` diff --git a/plugins/lighthouse-backend/api-report.md b/plugins/lighthouse-backend/api-report.md new file mode 100644 index 0000000000..a91e47dfc5 --- /dev/null +++ b/plugins/lighthouse-backend/api-report.md @@ -0,0 +1,29 @@ +## API Report File for "@backstage/plugin-lighthouse-backend" + +> 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 { Config } from '@backstage/config'; +import { Logger } from 'winston'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; + +// @public (undocumented) +export interface CreateLighthouseSchedulerOptions { + // (undocumented) + catalogClient: CatalogClient; + // (undocumented) + config: Config; + // (undocumented) + logger: Logger; + // (undocumented) + scheduler?: PluginTaskScheduler; +} + +// @public (undocumented) +export function createScheduler( + options: CreateLighthouseSchedulerOptions, +): Promise<void>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/lighthouse-backend/package.json b/plugins/lighthouse-backend/package.json new file mode 100644 index 0000000000..0af3f7f5fb --- /dev/null +++ b/plugins/lighthouse-backend/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-lighthouse-backend", + "description": "Backend functionalities for lighthouse", + "version": "0.1.1-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/lighthouse-backend" + }, + "keywords": [ + "lighthouse" + ], + "files": [ + "dist" + ], + "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" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-lighthouse-common": "workspace:^", + "@backstage/types": "workspace:^", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/plugins/lighthouse-backend/src/config.ts b/plugins/lighthouse-backend/src/config.ts new file mode 100644 index 0000000000..bea1c35661 --- /dev/null +++ b/plugins/lighthouse-backend/src/config.ts @@ -0,0 +1,68 @@ +/* + * 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 { HumanDuration as HumanDuration } from '@backstage/types'; + +export interface LighthouseAuditScheduleConfig { + schedule: HumanDuration; + timeout: HumanDuration; + auditDetail: HumanDuration; +} + +/** @public */ +export type LighthouseAuditSchedule = { + getSchedule: () => HumanDuration; + getTimeout: () => HumanDuration; +}; + +/** @public */ +export class LighthouseAuditScheduleImpl implements LighthouseAuditSchedule { + static fromConfig(config: Config) { + const lighthouse = config.getOptionalConfig('lighthouse'); + + let schedule: HumanDuration = { days: 1 }; + let timeout: HumanDuration = {}; + + if (lighthouse) { + const scheduleConfig = lighthouse.getOptionalConfig('schedule'); + const timeoutConfig = lighthouse.getOptionalConfig('timeout'); + + if (scheduleConfig) { + schedule = scheduleConfig as HumanDuration; + } + + if (timeoutConfig) { + timeout = timeoutConfig as HumanDuration; + } + } + + return new LighthouseAuditScheduleImpl(schedule, timeout); + } + + constructor( + private schedule: HumanDuration, + private timeout: HumanDuration, + ) {} + + getSchedule(): HumanDuration { + return this.schedule; + } + + getTimeout(): HumanDuration { + return this.timeout; + } +} diff --git a/packages/core-components/src/components/Button/index.ts b/plugins/lighthouse-backend/src/index.ts similarity index 88% rename from packages/core-components/src/components/Button/index.ts rename to plugins/lighthouse-backend/src/index.ts index b3dc40e6e7..ee878b9214 100644 --- a/packages/core-components/src/components/Button/index.ts +++ b/plugins/lighthouse-backend/src/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Button } from './Button'; -export type { ButtonProps } from './Button'; + +export * from './service/plugin'; diff --git a/plugins/lighthouse-backend/src/service/EntitiesLoader.ts b/plugins/lighthouse-backend/src/service/EntitiesLoader.ts new file mode 100644 index 0000000000..fe9aab48cc --- /dev/null +++ b/plugins/lighthouse-backend/src/service/EntitiesLoader.ts @@ -0,0 +1,32 @@ +/* + * 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, + CATALOG_FILTER_EXISTS, +} from '@backstage/catalog-client'; + +export async function loadLighthouseEntities(catalogClient: CatalogClient) { + const filter: Record<string, symbol | string> = { + kind: 'Component', + 'spec.type': 'website', + ['lighthouse.com/website-url']: CATALOG_FILTER_EXISTS, + }; + + return await catalogClient.getEntities({ + filter: [filter], + }); +} diff --git a/plugins/lighthouse-backend/src/service/plugin.ts b/plugins/lighthouse-backend/src/service/plugin.ts new file mode 100644 index 0000000000..8e8952a3d6 --- /dev/null +++ b/plugins/lighthouse-backend/src/service/plugin.ts @@ -0,0 +1,128 @@ +/* + * 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 { Logger } from 'winston'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { + CATALOG_FILTER_EXISTS, + CatalogClient, +} from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { LighthouseRestApi } from '@backstage/plugin-lighthouse-common'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { LighthouseAuditScheduleImpl } from '../config'; + +/** @public **/ +export interface CreateLighthouseSchedulerOptions { + logger: Logger; + config: Config; + scheduler?: PluginTaskScheduler; + catalogClient: CatalogClient; +} + +/** @public **/ +export async function createScheduler( + options: CreateLighthouseSchedulerOptions, +) { + const { logger, scheduler, catalogClient, config } = options; + const lighthouseApi = LighthouseRestApi.fromConfig(config); + + const lighthouseAuditConfig = LighthouseAuditScheduleImpl.fromConfig(config); + const formFactorToScreenEmulationMap = { + // the default is mobile, so no need to override + mobile: undefined, + // Values from lighthouse's cli "desktop" preset + // https://github.com/GoogleChrome/lighthouse/blob/a6738e0033e7e5ca308b97c1c36f298b7d399402/lighthouse-core/config/constants.js#L71-L77 + desktop: { + mobile: false, + width: 1350, + height: 940, + deviceScaleFactor: 1, + disabled: false, + }, + }; + + logger.info( + `Running with Scheduler Config ${JSON.stringify( + lighthouseAuditConfig.getSchedule(), + )} and timeout ${JSON.stringify(lighthouseAuditConfig.getTimeout())}`, + ); + + if (scheduler) { + await scheduler.scheduleTask({ + id: 'lighthouse_audit', + frequency: lighthouseAuditConfig.getSchedule(), + timeout: lighthouseAuditConfig.getTimeout(), + initialDelay: { minutes: 15 }, + fn: async () => { + const filter: Record<string, symbol | string> = { + kind: 'Component', + 'spec.type': 'website', + ['metadata.annotations.lighthouse.com/website-url']: + CATALOG_FILTER_EXISTS, + }; + + logger.info('Running Lighthouse Audit Task'); + + const websitesWithUrl = await catalogClient.getEntities({ + filter: [filter], + }); + + let index = 0; + for (const entity of websitesWithUrl.items) { + const websiteUrl = + entity.metadata.annotations?.['lighthouse.com/website-url'] ?? ''; + + if (!websiteUrl) { + continue; + } + + const controller = new AbortController(); + + await scheduler.scheduleTask({ + id: `lighthouse_audit_${stringifyEntityRef(entity)}`, + frequency: {}, + timeout: {}, + initialDelay: { minutes: index * 2 }, + signal: controller.signal, + fn: async () => { + logger.info( + `Processing Website Url ${websiteUrl} for Entity ${entity.metadata.name}`, + ); + + await lighthouseApi.triggerAudit({ + url: websiteUrl, + options: { + lighthouseConfig: { + settings: { + formFactor: 'mobile', + emulatedFormFactor: 'mobile', + screenEmulation: formFactorToScreenEmulationMap.mobile, + }, + }, + }, + }); + + logger.info('Stop Scheduled Task'); + controller.abort(); + }, + }); + index++; + } + }, + }); + } +} diff --git a/plugins/lighthouse-backend/src/setupTests.ts b/plugins/lighthouse-backend/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/lighthouse-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/lighthouse-common/.eslintrc.js b/plugins/lighthouse-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/lighthouse-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/lighthouse-common/CHANGELOG.md b/plugins/lighthouse-common/CHANGELOG.md new file mode 100644 index 0000000000..6fb22efc9f --- /dev/null +++ b/plugins/lighthouse-common/CHANGELOG.md @@ -0,0 +1,23 @@ +# @backstage/plugin-lighthouse-common + +## 0.1.0 + +### Minor Changes + +- eef62546ce: Introduce @backstage/plugin-lighthouse-common with the API implementation + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + +## 0.1.0-next.0 + +### Minor Changes + +- eef62546ce: Introduce @backstage/plugin-lighthouse-common with the API implementation + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 diff --git a/plugins/lighthouse-common/README.md b/plugins/lighthouse-common/README.md new file mode 100644 index 0000000000..9ab6d9a86d --- /dev/null +++ b/plugins/lighthouse-common/README.md @@ -0,0 +1,3 @@ +# @backstage/plugin-lighthouse-common + +Common types and functionalities for lighthouse, to be shared between lighthouse and lighthouse-backend. diff --git a/plugins/lighthouse-common/api-report.md b/plugins/lighthouse-common/api-report.md new file mode 100644 index 0000000000..8589c20d2a --- /dev/null +++ b/plugins/lighthouse-common/api-report.md @@ -0,0 +1,161 @@ +## API Report File for "@backstage/plugin-lighthouse-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; + +// @public (undocumented) +export type Audit = AuditRunning | AuditFailed | AuditCompleted; + +// @public (undocumented) +export interface AuditBase { + // (undocumented) + id: string; + // (undocumented) + timeCreated: string; + // (undocumented) + url: string; +} + +// @public (undocumented) +export interface AuditCompleted extends AuditBase { + // (undocumented) + categories: Record<LighthouseCategoryId, LighthouseCategoryAbbr>; + // (undocumented) + report: Object; + // (undocumented) + status: 'COMPLETED'; + // (undocumented) + timeCompleted: string; +} + +// @public (undocumented) +export interface AuditFailed extends AuditBase { + // (undocumented) + status: 'FAILED'; + // (undocumented) + timeCompleted: string; +} + +// @public (undocumented) +export interface AuditRunning extends AuditBase { + // (undocumented) + status: 'RUNNING'; +} + +// @public (undocumented) +export class FetchError extends Error { + // (undocumented) + static forResponse(resp: Response): Promise<FetchError>; + // (undocumented) + get name(): string; +} + +// @public (undocumented) +export type FormFactor = 'mobile' | 'desktop'; + +// @public (undocumented) +export interface LASListRequest { + // (undocumented) + limit?: number; + // (undocumented) + offset?: number; +} + +// @public (undocumented) +export interface LASListResponse<Item> { + // (undocumented) + items: Item[]; + // (undocumented) + limit: number; + // (undocumented) + offset: number; + // (undocumented) + total: number; +} + +// @public (undocumented) +export type LighthouseApi = { + url: string; + getWebsiteList: (listOptions: LASListRequest) => Promise<WebsiteListResponse>; + getWebsiteForAuditId: (auditId: string) => Promise<Website>; + triggerAudit: (payload: TriggerAuditPayload) => Promise<Audit>; + getWebsiteByUrl: (websiteUrl: string) => Promise<Website>; +}; + +// @public (undocumented) +export interface LighthouseCategoryAbbr { + // (undocumented) + id: LighthouseCategoryId; + // (undocumented) + score: number; + // (undocumented) + title: string; +} + +// @public (undocumented) +export type LighthouseCategoryId = + | 'pwa' + | 'seo' + | 'performance' + | 'accessibility' + | 'best-practices'; + +// @public (undocumented) +export type LighthouseConfigSettings = { + formFactor: FormFactor; + screenEmulation: + | { + mobile: boolean; + width: number; + height: number; + deviceScaleFactor: number; + disabled: boolean; + } + | undefined; + emulatedFormFactor: FormFactor; +}; + +// @public (undocumented) +export class LighthouseRestApi implements LighthouseApi { + constructor(url: string); + // (undocumented) + static fromConfig(config: Config): LighthouseRestApi; + // (undocumented) + getWebsiteByUrl(websiteUrl: string): Promise<Website>; + // (undocumented) + getWebsiteForAuditId(auditId: string): Promise<Website>; + // (undocumented) + getWebsiteList(options?: LASListRequest): Promise<WebsiteListResponse>; + // (undocumented) + triggerAudit(payload: TriggerAuditPayload): Promise<Audit>; + // (undocumented) + url: string; +} + +// @public (undocumented) +export interface TriggerAuditPayload { + // (undocumented) + options: { + lighthouseConfig: { + settings: LighthouseConfigSettings; + }; + }; + // (undocumented) + url: string; +} + +// @public (undocumented) +export interface Website { + // (undocumented) + audits: Audit[]; + // (undocumented) + lastAudit: Audit; + // (undocumented) + url: string; +} + +// @public (undocumented) +export type WebsiteListResponse = LASListResponse<Website>; +``` diff --git a/plugins/lighthouse-common/package.json b/plugins/lighthouse-common/package.json new file mode 100644 index 0000000000..07061fae69 --- /dev/null +++ b/plugins/lighthouse-common/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-lighthouse-common", + "description": "Common functionalities for lighthouse, to be shared between lighthouse and lighthouse-backend plugin", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/lighthouse-common" + }, + "keywords": [ + "lighthouse" + ], + "files": [ + "dist" + ], + "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" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@backstage/config": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/plugins/lighthouse-common/src/api.ts b/plugins/lighthouse-common/src/api.ts new file mode 100644 index 0000000000..0abd6dacdc --- /dev/null +++ b/plugins/lighthouse-common/src/api.ts @@ -0,0 +1,188 @@ +/* + * 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'; + +/** @public */ +export type LighthouseCategoryId = + | 'pwa' + | 'seo' + | 'performance' + | 'accessibility' + | 'best-practices'; + +/** @public */ +export interface LighthouseCategoryAbbr { + id: LighthouseCategoryId; + score: number; + title: string; +} + +/** @public */ +export interface LASListRequest { + offset?: number; + limit?: number; +} + +/** @public */ +export interface LASListResponse<Item> { + items: Item[]; + total: number; + offset: number; + limit: number; +} + +/** @public */ +export interface AuditBase { + id: string; + url: string; + timeCreated: string; +} + +/** @public */ +export interface AuditRunning extends AuditBase { + status: 'RUNNING'; +} + +/** @public */ +export interface AuditFailed extends AuditBase { + status: 'FAILED'; + timeCompleted: string; +} + +/** @public */ +export interface AuditCompleted extends AuditBase { + status: 'COMPLETED'; + timeCompleted: string; + report: Object; + categories: Record<LighthouseCategoryId, LighthouseCategoryAbbr>; +} + +/** @public */ +export type Audit = AuditRunning | AuditFailed | AuditCompleted; + +/** @public */ +export interface Website { + url: string; + audits: Audit[]; + lastAudit: Audit; +} + +/** @public */ +export type WebsiteListResponse = LASListResponse<Website>; + +/** @public */ +export type FormFactor = 'mobile' | 'desktop'; + +/** @public */ +export type LighthouseConfigSettings = { + // For lighthouse 7+ + formFactor: FormFactor; + screenEmulation: + | { + mobile: boolean; + width: number; + height: number; + deviceScaleFactor: number; + disabled: boolean; + } + | undefined; + // For lighthouse before 7 + emulatedFormFactor: FormFactor; +}; + +/** @public */ +export interface TriggerAuditPayload { + url: string; + options: { + lighthouseConfig: { + settings: LighthouseConfigSettings; + }; + }; +} + +/** @public */ +export class FetchError extends Error { + get name(): string { + return this.constructor.name; + } + + static async forResponse(resp: Response): Promise<FetchError> { + return new FetchError( + `Request failed with status code ${ + resp.status + }.\nReason: ${await resp.text()}`, + ); + } +} + +/** @public */ +export type LighthouseApi = { + url: string; + getWebsiteList: (listOptions: LASListRequest) => Promise<WebsiteListResponse>; + getWebsiteForAuditId: (auditId: string) => Promise<Website>; + triggerAudit: (payload: TriggerAuditPayload) => Promise<Audit>; + getWebsiteByUrl: (websiteUrl: string) => Promise<Website>; +}; + +/** @public */ +export class LighthouseRestApi implements LighthouseApi { + static fromConfig(config: Config) { + return new LighthouseRestApi(config.getString('lighthouse.baseUrl')); + } + + constructor(public url: string) {} + + private async fetch<T = any>(input: string, init?: RequestInit): Promise<T> { + const resp = await fetch(`${this.url}${input}`, init); + if (!resp.ok) throw await FetchError.forResponse(resp); + return await resp.json(); + } + + async getWebsiteList( + options: LASListRequest = {}, + ): Promise<WebsiteListResponse> { + const { limit, offset } = options; + const params = new URLSearchParams(); + if (typeof limit === 'number') params.append('limit', limit.toString()); + if (typeof offset === 'number') params.append('offset', offset.toString()); + return await this.fetch<WebsiteListResponse>( + `/v1/websites?${params.toString()}`, + ); + } + + async getWebsiteForAuditId(auditId: string): Promise<Website> { + return await this.fetch<Website>( + `/v1/audits/${encodeURIComponent(auditId)}/website`, + ); + } + + async triggerAudit(payload: TriggerAuditPayload): Promise<Audit> { + return await this.fetch<Audit>('/v1/audits', { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + async getWebsiteByUrl(websiteUrl: string): Promise<Website> { + return this.fetch<Website>( + `/v1/websites/${encodeURIComponent(websiteUrl)}`, + ); + } +} diff --git a/plugins/lighthouse-common/src/index.ts b/plugins/lighthouse-common/src/index.ts new file mode 100644 index 0000000000..41b839087f --- /dev/null +++ b/plugins/lighthouse-common/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. + */ + +/** + * Common functionalities for Lighthouse, to be shared between the `lighthouse` and `lighthouse-backend` plugins + * + * @packageDocumentation + */ + +export * from './api'; diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 5f0d1510ba..e2a035dabf 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,112 @@ # @backstage/plugin-lighthouse +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-lighthouse-common@0.1.0 + +## 0.4.0 + +### Minor Changes + +- eef62546ce: Require @backstage/plugin-lighthouse-common package where API implementation moved to. + + **BREAKING**: the following types have been moved to @backstage/plugin-lighthouse-common: + + - `Audit` + - `AuditBase` + - `AuditCompleted` + - `AuditFailed` + - `AuditRunning` + - `FetchError` + - `LASListRequest` + - `LASListResponse` + - `LighthouseApi` + - `LighthouseCategoryAbbr` + - `LighthouseCategoryId` + - `LighthouseConfigSettings` + - `LighthouseRestApi` + - `TriggerAuditPayload` + - `Website` + - `WebsiteListResponse` + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- 4bdd6bcb3c: Prefer Link over plain anchors +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/plugin-lighthouse-common@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/config@1.0.6 + +## 0.4.0-next.2 + +### Minor Changes + +- eef62546ce: Require @backstage/plugin-lighthouse-common package where API implementation moved to. + + **BREAKING**: the following types have been moved to @backstage/plugin-lighthouse-common: + + - `Audit` + - `AuditBase` + - `AuditCompleted` + - `AuditFailed` + - `AuditRunning` + - `FetchError` + - `LASListRequest` + - `LASListResponse` + - `LighthouseApi` + - `LighthouseCategoryAbbr` + - `LighthouseCategoryId` + - `LighthouseConfigSettings` + - `LighthouseRestApi` + - `TriggerAuditPayload` + - `Website` + - `WebsiteListResponse` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-common@0.1.0-next.0 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/lighthouse/README.md b/plugins/lighthouse/README.md index f07041d9ff..1d2385e4c4 100644 --- a/plugins/lighthouse/README.md +++ b/plugins/lighthouse/README.md @@ -105,3 +105,7 @@ const overviewContent = ( </EntitySwitch.Case> </EntitySwitch> ``` + +## Schedule + +If you want to run automated scheduled runs, you can install the [@backstage/lighthouse-backend](../lighthouse-backend/README.md) plugin diff --git a/plugins/lighthouse/api-report.md b/plugins/lighthouse/api-report.md index e9089c7e92..4ab851472a 100644 --- a/plugins/lighthouse/api-report.md +++ b/plugins/lighthouse/api-report.md @@ -7,50 +7,11 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import { InfoCardVariants } from '@backstage/core-components'; +import { LighthouseApi } from '@backstage/plugin-lighthouse-common'; import { RouteRef } from '@backstage/core-plugin-api'; -// @public (undocumented) -export type Audit = AuditRunning | AuditFailed | AuditCompleted; - -// @public (undocumented) -export interface AuditBase { - // (undocumented) - id: string; - // (undocumented) - timeCreated: string; - // (undocumented) - url: string; -} - -// @public (undocumented) -export interface AuditCompleted extends AuditBase { - // (undocumented) - categories: Record<LighthouseCategoryId, LighthouseCategoryAbbr>; - // (undocumented) - report: Object; - // (undocumented) - status: 'COMPLETED'; - // (undocumented) - timeCompleted: string; -} - -// @public (undocumented) -export interface AuditFailed extends AuditBase { - // (undocumented) - status: 'FAILED'; - // (undocumented) - timeCompleted: string; -} - -// @public (undocumented) -export interface AuditRunning extends AuditBase { - // (undocumented) - status: 'RUNNING'; -} - // @public (undocumented) export const EmbeddedRouter: () => JSX.Element; @@ -63,93 +24,20 @@ export const EntityLastLighthouseAuditCard: (props: { // @public (undocumented) export const EntityLighthouseContent: () => JSX.Element; -// @public (undocumented) -export class FetchError extends Error { - // (undocumented) - static forResponse(resp: Response): Promise<FetchError>; - // (undocumented) - get name(): string; -} - -// @public (undocumented) -export type FormFactor = 'mobile' | 'desktop'; - // @public (undocumented) const isLighthouseAvailable: (entity: Entity) => boolean; export { isLighthouseAvailable }; export { isLighthouseAvailable as isPluginApplicableToEntity }; -// @public (undocumented) -export interface LASListRequest { - // (undocumented) - limit?: number; - // (undocumented) - offset?: number; -} - -// @public (undocumented) -export interface LASListResponse<Item> { - // (undocumented) - items: Item[]; - // (undocumented) - limit: number; - // (undocumented) - offset: number; - // (undocumented) - total: number; -} - // @public (undocumented) export const LastLighthouseAuditCard: (props: { dense?: boolean; variant?: InfoCardVariants; }) => JSX.Element; -// @public (undocumented) -export type LighthouseApi = { - url: string; - getWebsiteList: (listOptions: LASListRequest) => Promise<WebsiteListResponse>; - getWebsiteForAuditId: (auditId: string) => Promise<Website>; - triggerAudit: (payload: TriggerAuditPayload) => Promise<Audit>; - getWebsiteByUrl: (websiteUrl: string) => Promise<Website>; -}; - // @public (undocumented) export const lighthouseApiRef: ApiRef<LighthouseApi>; -// @public (undocumented) -export interface LighthouseCategoryAbbr { - // (undocumented) - id: LighthouseCategoryId; - // (undocumented) - score: number; - // (undocumented) - title: string; -} - -// @public (undocumented) -export type LighthouseCategoryId = - | 'pwa' - | 'seo' - | 'performance' - | 'accessibility' - | 'best-practices'; - -// @public (undocumented) -export type LighthouseConfigSettings = { - formFactor: FormFactor; - screenEmulation: - | { - mobile: boolean; - width: number; - height: number; - deviceScaleFactor: number; - disabled: boolean; - } - | undefined; - emulatedFormFactor: FormFactor; -}; - // @public (undocumented) export const LighthousePage: () => JSX.Element; @@ -165,48 +53,6 @@ const lighthousePlugin: BackstagePlugin< export { lighthousePlugin }; export { lighthousePlugin as plugin }; -// @public (undocumented) -export class LighthouseRestApi implements LighthouseApi { - constructor(url: string); - // (undocumented) - static fromConfig(config: Config): LighthouseRestApi; - // (undocumented) - getWebsiteByUrl(websiteUrl: string): Promise<Website>; - // (undocumented) - getWebsiteForAuditId(auditId: string): Promise<Website>; - // (undocumented) - getWebsiteList(options?: LASListRequest): Promise<WebsiteListResponse>; - // (undocumented) - triggerAudit(payload: TriggerAuditPayload): Promise<Audit>; - // (undocumented) - url: string; -} - // @public (undocumented) export const Router: () => JSX.Element; - -// @public (undocumented) -export interface TriggerAuditPayload { - // (undocumented) - options: { - lighthouseConfig: { - settings: LighthouseConfigSettings; - }; - }; - // (undocumented) - url: string; -} - -// @public (undocumented) -export interface Website { - // (undocumented) - audits: Audit[]; - // (undocumented) - lastAudit: Audit; - // (undocumented) - url: string; -} - -// @public (undocumented) -export type WebsiteListResponse = LASListResponse<Website>; ``` diff --git a/plugins/lighthouse/dev/index.tsx b/plugins/lighthouse/dev/index.tsx index b5e7dd085e..41dd8761d9 100644 --- a/plugins/lighthouse/dev/index.tsx +++ b/plugins/lighthouse/dev/index.tsx @@ -16,7 +16,8 @@ import { createDevApp } from '@backstage/dev-utils'; import { lighthousePlugin } from '../src/plugin'; -import { lighthouseApiRef, LighthouseRestApi } from '../src'; +import { lighthouseApiRef } from '../src'; +import { LighthouseRestApi } from '@backstage/plugin-lighthouse-common'; createDevApp() .registerPlugin(lighthousePlugin) diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 4e67412b17..0d9d1365eb 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.3.14", + "version": "0.4.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,6 +38,7 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-lighthouse-common": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -60,7 +61,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/lighthouse/src/api.ts b/plugins/lighthouse/src/api.ts index a7197c4b96..8912f4838b 100644 --- a/plugins/lighthouse/src/api.ts +++ b/plugins/lighthouse/src/api.ts @@ -14,181 +14,10 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; import { createApiRef } from '@backstage/core-plugin-api'; - -/** @public */ -export type LighthouseCategoryId = - | 'pwa' - | 'seo' - | 'performance' - | 'accessibility' - | 'best-practices'; - -/** @public */ -export interface LighthouseCategoryAbbr { - id: LighthouseCategoryId; - score: number; - title: string; -} - -/** @public */ -export interface LASListRequest { - offset?: number; - limit?: number; -} - -/** @public */ -export interface LASListResponse<Item> { - items: Item[]; - total: number; - offset: number; - limit: number; -} - -/** @public */ -export interface AuditBase { - id: string; - url: string; - timeCreated: string; -} - -/** @public */ -export interface AuditRunning extends AuditBase { - status: 'RUNNING'; -} - -/** @public */ -export interface AuditFailed extends AuditBase { - status: 'FAILED'; - timeCompleted: string; -} - -/** @public */ -export interface AuditCompleted extends AuditBase { - status: 'COMPLETED'; - timeCompleted: string; - report: Object; - categories: Record<LighthouseCategoryId, LighthouseCategoryAbbr>; -} - -/** @public */ -export type Audit = AuditRunning | AuditFailed | AuditCompleted; - -/** @public */ -export interface Website { - url: string; - audits: Audit[]; - lastAudit: Audit; -} - -/** @public */ -export type WebsiteListResponse = LASListResponse<Website>; - -/** @public */ -export type FormFactor = 'mobile' | 'desktop'; - -/** @public */ -export type LighthouseConfigSettings = { - // For lighthouse 7+ - formFactor: FormFactor; - screenEmulation: - | { - mobile: boolean; - width: number; - height: number; - deviceScaleFactor: number; - disabled: boolean; - } - | undefined; - // For lighthouse before 7 - emulatedFormFactor: FormFactor; -}; - -/** @public */ -export interface TriggerAuditPayload { - url: string; - options: { - lighthouseConfig: { - settings: LighthouseConfigSettings; - }; - }; -} - -/** @public */ -export class FetchError extends Error { - get name(): string { - return this.constructor.name; - } - - static async forResponse(resp: Response): Promise<FetchError> { - return new FetchError( - `Request failed with status code ${ - resp.status - }.\nReason: ${await resp.text()}`, - ); - } -} - -/** @public */ -export type LighthouseApi = { - url: string; - getWebsiteList: (listOptions: LASListRequest) => Promise<WebsiteListResponse>; - getWebsiteForAuditId: (auditId: string) => Promise<Website>; - triggerAudit: (payload: TriggerAuditPayload) => Promise<Audit>; - getWebsiteByUrl: (websiteUrl: string) => Promise<Website>; -}; +import { LighthouseApi } from '@backstage/plugin-lighthouse-common'; /** @public */ export const lighthouseApiRef = createApiRef<LighthouseApi>({ id: 'plugin.lighthouse.service', }); - -/** @public */ -export class LighthouseRestApi implements LighthouseApi { - static fromConfig(config: Config) { - return new LighthouseRestApi(config.getString('lighthouse.baseUrl')); - } - - constructor(public url: string) {} - - private async fetch<T = any>(input: string, init?: RequestInit): Promise<T> { - const resp = await fetch(`${this.url}${input}`, init); - if (!resp.ok) throw await FetchError.forResponse(resp); - return await resp.json(); - } - - async getWebsiteList( - options: LASListRequest = {}, - ): Promise<WebsiteListResponse> { - const { limit, offset } = options; - const params = new URLSearchParams(); - if (typeof limit === 'number') params.append('limit', limit.toString()); - if (typeof offset === 'number') params.append('offset', offset.toString()); - return await this.fetch<WebsiteListResponse>( - `/v1/websites?${params.toString()}`, - ); - } - - async getWebsiteForAuditId(auditId: string): Promise<Website> { - return await this.fetch<Website>( - `/v1/audits/${encodeURIComponent(auditId)}/website`, - ); - } - - async triggerAudit(payload: TriggerAuditPayload): Promise<Audit> { - return await this.fetch<Audit>('/v1/audits', { - method: 'POST', - body: JSON.stringify(payload), - headers: { - 'Content-Type': 'application/json', - }, - }); - } - - async getWebsiteByUrl(websiteUrl: string): Promise<Website> { - return this.fetch<Website>( - `/v1/websites/${encodeURIComponent(websiteUrl)}`, - ); - } -} diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx index 59892e1475..d1ed72c3fb 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx @@ -19,10 +19,10 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { - lighthouseApiRef, LighthouseRestApi, WebsiteListResponse, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import * as data from '../../__fixtures__/website-list-response.json'; import { AuditListForEntity } from './AuditListForEntity'; diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx index c2ee202841..45889a2cd3 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx @@ -25,9 +25,9 @@ import AuditListTable from './AuditListTable'; import { WebsiteListResponse, - lighthouseApiRef, LighthouseRestApi, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { formatTime } from '../../utils'; import { setupServer } from 'msw/node'; import * as data from '../../__fixtures__/website-list-response.json'; diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx index 9f5fa94436..8f718cda64 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx @@ -15,7 +15,8 @@ */ import React, { useState, useEffect } from 'react'; import Typography from '@material-ui/core/Typography'; -import { Website, lighthouseApiRef } from '../../api'; +import { Website } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import useInterval from 'react-use/lib/useInterval'; import { formatTime, diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx index 2b422cce7a..55bde9aebd 100644 --- a/plugins/lighthouse/src/components/AuditList/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx @@ -33,10 +33,10 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; import { - lighthouseApiRef, LighthouseRestApi, WebsiteListResponse, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import * as data from '../../__fixtures__/website-list-response.json'; import AuditList from './index'; import { ApiProvider } from '@backstage/core-app-api'; diff --git a/plugins/lighthouse/src/components/AuditStatusIcon/index.tsx b/plugins/lighthouse/src/components/AuditStatusIcon/index.tsx index 52ba2e00cf..b0e1c876ae 100644 --- a/plugins/lighthouse/src/components/AuditStatusIcon/index.tsx +++ b/plugins/lighthouse/src/components/AuditStatusIcon/index.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { Audit } from '../../api'; +import { Audit } from '@backstage/plugin-lighthouse-common'; import { StatusError, StatusOK, diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index d555133f3e..7e7d8e4b7d 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -38,7 +38,12 @@ import { render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; -import { Audit, lighthouseApiRef, LighthouseRestApi, Website } from '../../api'; +import { + Audit, + LighthouseRestApi, + Website, +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { formatTime } from '../../utils'; import * as data from '../../__fixtures__/website-response.json'; import AuditView from './index'; diff --git a/plugins/lighthouse/src/components/AuditView/index.tsx b/plugins/lighthouse/src/components/AuditView/index.tsx index ade485b09a..8d82f920cd 100644 --- a/plugins/lighthouse/src/components/AuditView/index.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.tsx @@ -33,7 +33,8 @@ import { useParams, } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; -import { Audit, lighthouseApiRef, Website } from '../../api'; +import { Audit, Website } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { formatTime } from '../../utils'; import AuditStatusIcon from '../AuditStatusIcon'; import LighthouseSupportButton from '../SupportButton'; diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx index c59458f3f3..09b21fd028 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx @@ -22,7 +22,7 @@ import { AuditCompleted, LighthouseCategoryId, WebsiteListResponse, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import * as data from '../../__fixtures__/website-list-response.json'; import { LastLighthouseAuditCard } from './LastLighthouseAuditCard'; diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx index 1562031007..af5ce94487 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx @@ -15,7 +15,11 @@ */ import React from 'react'; import Typography from '@material-ui/core/Typography'; -import { Audit, AuditCompleted, LighthouseCategoryId } from '../../api'; +import { + Audit, + AuditCompleted, + LighthouseCategoryId, +} from '@backstage/plugin-lighthouse-common'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import AuditStatusIcon from '../AuditStatusIcon'; import { diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index 60a7bc1301..1b394a30f0 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -32,7 +32,8 @@ import { fireEvent, render, waitFor } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; -import { Audit, lighthouseApiRef, LighthouseRestApi } from '../../api'; +import { Audit, LighthouseRestApi } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import * as data from '../../__fixtures__/create-audit-response.json'; import CreateAudit from './index'; diff --git a/plugins/lighthouse/src/components/CreateAudit/index.tsx b/plugins/lighthouse/src/components/CreateAudit/index.tsx index 8ed7dc8996..a76b031796 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.tsx @@ -27,9 +27,9 @@ import React, { useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FormFactor, - lighthouseApiRef, LighthouseConfigSettings, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { useQuery } from '../../utils'; import LighthouseSupportButton from '../SupportButton'; diff --git a/plugins/lighthouse/src/components/Intro/index.tsx b/plugins/lighthouse/src/components/Intro/index.tsx index 57609476b8..8701c473aa 100644 --- a/plugins/lighthouse/src/components/Intro/index.tsx +++ b/plugins/lighthouse/src/components/Intro/index.tsx @@ -22,6 +22,7 @@ import LighthouseSupportButton from '../SupportButton'; import { ContentHeader, InfoCard, + Link, MarkdownContent, } from '@backstage/core-components'; @@ -50,8 +51,8 @@ with the environment variable \`LAS_CORS\` set to \`true\`._ When you have an instance running that Backstage can hook into, first install the plugin into your app: \`\`\`sh -cd packages/app -yarn add @backstage/plugin-lighthouse +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-lighthouse \`\`\` Modify your app routes in \`App.tsx\` to include the \`LighthousePage\` component exported from the plugin, for example: @@ -108,10 +109,9 @@ function GettingStartedCard() { <Grid container direction="row" justifyContent="flex-end"> <Grid item> <Button - component="a" - href="https://github.com/spotify/lighthouse-audit-service" + component={Link} + to="https://github.com/spotify/lighthouse-audit-service" size="small" - target="_blank" > Check out the README </Button> diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx index 13da0f6f4f..7ab717c57d 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx @@ -17,7 +17,8 @@ import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; -import { lighthouseApiRef, WebsiteListResponse } from '../api'; +import { WebsiteListResponse } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../api'; import * as data from '../__fixtures__/website-list-response.json'; import { useWebsiteForEntity } from './useWebsiteForEntity'; diff --git a/plugins/lighthouse/src/plugin.ts b/plugins/lighthouse/src/plugin.ts index 24052f7771..3458ae7240 100644 --- a/plugins/lighthouse/src/plugin.ts +++ b/plugins/lighthouse/src/plugin.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { lighthouseApiRef, LighthouseRestApi } from './api'; +import { LighthouseRestApi } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from './api'; import { createPlugin, createRouteRef, diff --git a/plugins/lighthouse/src/utils.ts b/plugins/lighthouse/src/utils.ts index b255f2f90e..918995386d 100644 --- a/plugins/lighthouse/src/utils.ts +++ b/plugins/lighthouse/src/utils.ts @@ -14,7 +14,12 @@ * limitations under the License. */ import { useLocation } from 'react-router-dom'; -import { Website, Audit, LighthouseCategoryId, AuditCompleted } from './api'; +import { + Website, + Audit, + LighthouseCategoryId, + AuditCompleted, +} from '@backstage/plugin-lighthouse-common'; export function useQuery(): URLSearchParams { return new URLSearchParams(useLocation().search); diff --git a/plugins/linguist-backend/.eslintrc.js b/plugins/linguist-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/linguist-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/linguist-backend/CHANGELOG.md b/plugins/linguist-backend/CHANGELOG.md new file mode 100644 index 0000000000..102c42c4cc --- /dev/null +++ b/plugins/linguist-backend/CHANGELOG.md @@ -0,0 +1,40 @@ +# @backstage/plugin-linguist-backend + +## 0.2.0-next.0 + +### Minor Changes + +- 4a1c318853: **BREAKING** The linguist-backend `createRouter` now requires that the `tokenManger` is passed to the router. + +### Patch Changes + +- 2ea5782162: Fixed bug in LinguistBackendApi that caused initial batch of entities to be skipped. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-linguist-common@0.1.0 + +## 0.1.0 + +### Minor Changes + +- 75cfee5688: Introduced the Linguist plugin, checkout the plugin's `README.md` for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-linguist-common@0.1.0 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 diff --git a/plugins/linguist-backend/README.md b/plugins/linguist-backend/README.md new file mode 100644 index 0000000000..a7c89d23b3 --- /dev/null +++ b/plugins/linguist-backend/README.md @@ -0,0 +1,104 @@ +# Linguist Backend + +Welcome to the Linguist backend plugin! This plugin provides data for the Linguist frontend features. + +## Setup + +The following sections will help you get the Linguist Backend plugin setup and running. + +### Up and Running + +Here's how to get the backend up and running: + +1. First we need to add the `@backstage/plugin-linguist-backend` package to your backend: + + ```sh + # From the Backstage root directory + cd packages/backend + yarn add @backstage/plugin-linguist-backend + ``` + +2. Then we will create a new file named `packages/backend/src/plugins/linguist.ts`, and add the + following to it: + + ```ts + import { TaskScheduleDefinition } from '@backstage/backend-tasks'; + import { createRouter } from '@backstage/plugin-linguist-backend'; + import { Router } from 'express'; + import type { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise<Router> { + const schedule: TaskScheduleDefinition = { + frequency: { minutes: 2 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 90 }, + }; + + return createRouter({ schedule: schedule }, { ...env }); + } + ``` + +3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`: + + ```ts + import linguist from './plugins/linguist'; + // ... + async function main() { + // ... + // Add this line under the other lines that follow the useHotMemoize pattern + const linguistEnv = useHotMemoize(module, () => createEnv('linguist')); + // ... + // Insert this line under the other lines that add their routers to apiRouter in the same way + apiRouter.use('/linguist', await linguist(linguistEnv)); + ``` + +4. Now run `yarn start-backend` from the repo root +5. Finally open `http://localhost:7007/api/linguist/health` in a browser and it should return `{"status":"ok"}` + +## Batch Size + +The Linguist backend is setup to process entities by acting as a queue where it will pull down all the applicable entities from the Catalog and add them to it's database (saving just the `entityRef`). Then it will grab the `n` oldest entities that have not been processed to determine their languages and process them. To control the batch size simply provide that to the `createRouter` function in your `packages/backend/src/plugins/linguist.ts` like this: + +```ts +return createRouter({ schedule: schedule, batchSize: 40 }, { ...env }); +``` + +**Note:** The default batch size is 20 + +## Kind + +The default setup only processes entities of kind `['API', 'Component', 'Template']`. To control the `kind` that are processed provide that to the `createRouter` function in your `packages/backend/src/plugins/linguist.ts` like this: + +```ts +return createRouter({ schedule: schedule, kind: ['Component'] }, { ...env }); +``` + +## Refresh + +The default setup will only generate the language breakdown for entities with the linguist annotation that have not been generated yet. If you want this process to also refresh the data you can do so by adding the `age` (as a `HumanDuration`) in your `packages/backend/src/plugins/linguist.ts` when you call `createRouter`: + +```ts +return createRouter({ schedule: schedule, age: { days: 30 } }, { ...env }); +``` + +With the `age` setup like this if the language breakdown is older than 15 days it will get regenerated. It's recommended that if you choose to use this configuration to set it to a large value - 30, 90, or 180 - as this data generally does not change drastically. + +## Use Source Location + +You may wish to use the `backstage.io/source-location` annotation over using the `backstage.io/linguist` as you may not be able to quickly add that annotation to your Entities. To do this you'll just need to set the `useSourceLocation` boolean to `true` in your `packages/backend/src/plugins/linguist.ts` when you call `createRouter`: + +```ts +return createRouter( + { schedule: schedule, useSourceLocation: true }, + { ...env }, +); +``` + +**Note:** This has the potential to cause a lot of processing, be very thoughtful about this before hand + +## Links + +- [Frontend part of the plugin](../linguist/README.md) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/linguist-backend/api-report.md b/plugins/linguist-backend/api-report.md new file mode 100644 index 0000000000..d3a77bc2d7 --- /dev/null +++ b/plugins/linguist-backend/api-report.md @@ -0,0 +1,109 @@ +## API Report File for "@backstage/plugin-linguist-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { EntityResults } from '@backstage/plugin-linguist-common'; +import express from 'express'; +import { HumanDuration } from '@backstage/types'; +import { Knex } from 'knex'; +import { Languages } from '@backstage/plugin-linguist-common'; +import { Logger } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { ProcessedEntity } from '@backstage/plugin-linguist-common'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; +import { TokenManager } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; + +// @public (undocumented) +export function createRouter( + pluginOptions: PluginOptions, + routerOptions: RouterOptions, +): Promise<express.Router>; + +// @public (undocumented) +export class LinguistBackendApi { + constructor( + logger: Logger, + store: LinguistBackendStore, + urlReader: UrlReader, + discovery: PluginEndpointDiscovery, + tokenManager: TokenManager, + age?: HumanDuration, + batchSize?: number, + useSourceLocation?: boolean, + kind?: string[], + ); + // (undocumented) + getEntityLanguages(entityRef: string): Promise<Languages>; + // (undocumented) + processEntities(): Promise<void>; +} + +// @public (undocumented) +export class LinguistBackendDatabase implements LinguistBackendStore { + constructor(db: Knex); + // (undocumented) + static create(knex: Knex): Promise<LinguistBackendStore>; + // (undocumented) + getEntityResults(entityRef: string): Promise<Languages>; + // (undocumented) + getProcessedEntities(): Promise<ProcessedEntity[] | []>; + // (undocumented) + getUnprocessedEntities(): Promise<string[] | []>; + // (undocumented) + insertEntityResults(entityLanguages: EntityResults): Promise<string>; + // (undocumented) + insertNewEntity(entityRef: string): Promise<void>; +} + +// @public (undocumented) +export interface LinguistBackendStore { + // (undocumented) + getEntityResults(entityRef: string): Promise<Languages>; + // (undocumented) + getProcessedEntities(): Promise<ProcessedEntity[] | []>; + // (undocumented) + getUnprocessedEntities(): Promise<string[] | []>; + // (undocumented) + insertEntityResults(entityLanguages: EntityResults): Promise<string>; + // (undocumented) + insertNewEntity(entityRef: string): Promise<void>; +} + +// @public (undocumented) +export interface PluginOptions { + // (undocumented) + age?: HumanDuration; + // (undocumented) + batchSize?: number; + // (undocumented) + kind?: string[]; + // (undocumented) + schedule?: TaskScheduleDefinition; + // (undocumented) + useSourceLocation?: boolean; +} + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + database: PluginDatabaseManager; + // (undocumented) + discovery: PluginEndpointDiscovery; + // (undocumented) + linguistBackendApi?: LinguistBackendApi; + // (undocumented) + logger: Logger; + // (undocumented) + reader: UrlReader; + // (undocumented) + scheduler?: PluginTaskScheduler; + // (undocumented) + tokenManager: TokenManager; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/linguist-backend/migrations/20221115_init.js b/plugins/linguist-backend/migrations/20221115_init.js new file mode 100644 index 0000000000..3bcf99e08b --- /dev/null +++ b/plugins/linguist-backend/migrations/20221115_init.js @@ -0,0 +1,63 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // Note for the reader: the knex increments types automatically make it a + // primary column, whether you like it or not. That's why the id column is + // not marked as primary as one might have expected; it's only used for + // lookups by ID. Because, SQLite and MySQL don't return RETURNING on + // inserts ... so we want a manually generated key for lookups (an uuid), + // and also an index for ordering guarantees :) + await knex.schema.createTable('entity_result', table => { + table.comment('Table containing results from running Linguist'); + table + .bigIncrements('index') + .notNullable() + .comment('An insert counter to ensure ordering'); + table.uuid('id').notNullable().comment('The ID of the Linguist result'); + table + .text('entity_ref') + .unique() + .notNullable() + .comment('The entity ref that this Linguist result applies to'); + table.text('languages').comment('The results json as a string'); + table + .dateTime('created_at') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this entry was created'); + table + .dateTime('processed_date') + .defaultTo(knex.fn.now()) + .comment('The timestamp when this entity was processed'); + table.index('index', 'entity_result_index_idx'); + table.index('entity_ref', 'entity_result_entity_ref_idx'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('entity_result', table => { + table.dropIndex([], 'entity_result_index_idx'); + table.dropIndex([], 'entity_result_entity_ref_idx'); + }); + await knex.schema.dropTable('entity_result'); +}; diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json new file mode 100644 index 0000000000..8a49f8dca2 --- /dev/null +++ b/plugins/linguist-backend/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-linguist-backend", + "version": "0.2.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-linguist-common": "workspace:^", + "@backstage/types": "workspace:^", + "@types/express": "*", + "express": "^4.18.1", + "express-promise-router": "^4.1.0", + "fs-extra": "^10.0.0", + "knex": "^2.0.0", + "linguist-js": "^2.5.3", + "luxon": "^2.0.2", + "node-fetch": "^2.6.7", + "uuid": "^8.3.2", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.8", + "msw": "^1.0.0", + "supertest": "^6.2.4" + }, + "files": [ + "dist", + "migrations/**/*.{js,d.ts}" + ] +} diff --git a/plugins/linguist-backend/src/api/LinguistBackendApi.test.ts b/plugins/linguist-backend/src/api/LinguistBackendApi.test.ts new file mode 100644 index 0000000000..741d52e734 --- /dev/null +++ b/plugins/linguist-backend/src/api/LinguistBackendApi.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { kindOrDefault } from './LinguistBackendApi'; + +describe('kindOrDefault', () => { + it('should return default kind when undefined', () => { + expect(kindOrDefault()).toEqual(['API', 'Component', 'Template']); + }); + it('should return the default kind when empty', () => { + expect(kindOrDefault([])).toEqual(['API', 'Component', 'Template']); + }); + it('should return provided kind when not empty', () => { + expect(kindOrDefault(['API'])).toEqual(['API']); + }); +}); diff --git a/plugins/linguist-backend/src/api/LinguistBackendApi.ts b/plugins/linguist-backend/src/api/LinguistBackendApi.ts new file mode 100644 index 0000000000..6a4cc0ea56 --- /dev/null +++ b/plugins/linguist-backend/src/api/LinguistBackendApi.ts @@ -0,0 +1,240 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EntitiesOverview, + EntityResults, + Language, + Languages, +} from '@backstage/plugin-linguist-common'; +import { + CATALOG_FILTER_EXISTS, + CatalogClient, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { + PluginEndpointDiscovery, + TokenManager, + UrlReader, +} from '@backstage/backend-common'; + +import { DateTime } from 'luxon'; +import { LINGUIST_ANNOTATION } from '@backstage/plugin-linguist-common'; +import { LinguistBackendStore } from '../db'; +import { Logger } from 'winston'; +import fs from 'fs-extra'; +import linguist from 'linguist-js'; +import { + ANNOTATION_SOURCE_LOCATION, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { assertError } from '@backstage/errors'; +import { HumanDuration } from '@backstage/types'; + +/** @public */ +export class LinguistBackendApi { + private readonly logger: Logger; + private readonly store: LinguistBackendStore; + private readonly urlReader: UrlReader; + private readonly discovery: PluginEndpointDiscovery; + private readonly tokenManager: TokenManager; + + private readonly catalogClient: CatalogClient; + private readonly age?: HumanDuration; + private readonly batchSize?: number; + private readonly useSourceLocation?: boolean; + private readonly kind: string[]; + public constructor( + logger: Logger, + store: LinguistBackendStore, + urlReader: UrlReader, + discovery: PluginEndpointDiscovery, + tokenManager: TokenManager, + age?: HumanDuration, + batchSize?: number, + useSourceLocation?: boolean, + kind?: string[], + ) { + this.logger = logger; + this.store = store; + this.urlReader = urlReader; + this.discovery = discovery; + this.tokenManager = tokenManager; + this.catalogClient = new CatalogClient({ discoveryApi: this.discovery }); + this.batchSize = batchSize; + this.age = age; + this.useSourceLocation = useSourceLocation; + this.kind = kindOrDefault(kind); + } + + public async getEntityLanguages(entityRef: string): Promise<Languages> { + this.logger?.debug(`Getting languages for entity "${entityRef}"`); + + return this.store.getEntityResults(entityRef); + } + + public async processEntities() { + this.logger?.info('Updating list of entities'); + + await this.addNewEntities(); + + this.logger?.info('Processing applicable entities through Linguist'); + + await this.generateEntitiesLanguages(); + } + + private async addNewEntities() { + const annotationKey = this.useSourceLocation + ? ANNOTATION_SOURCE_LOCATION + : LINGUIST_ANNOTATION; + const request: GetEntitiesRequest = { + filter: { + kind: this.kind, + [`metadata.annotations.${annotationKey}`]: CATALOG_FILTER_EXISTS, + }, + fields: ['kind', 'metadata'], + }; + + const { token } = await this.tokenManager.getToken(); + const response = await this.catalogClient.getEntities(request, { token }); + const entities = response.items; + + entities.forEach(entity => { + const entityRef = stringifyEntityRef(entity); + this.store.insertNewEntity(entityRef); + }); + } + + private async generateEntitiesLanguages() { + const entitiesOverview = await this.getEntitiesOverview(); + this.logger?.info( + `Entities overview: Entity: ${entitiesOverview.entityCount}, Processed: ${entitiesOverview.processedCount}, Pending: ${entitiesOverview.pendingCount}, Stale ${entitiesOverview.staleCount}`, + ); + + const entities = entitiesOverview.filteredEntities.slice( + 0, + this.batchSize ?? 20, + ); + entities.forEach(async entityRef => { + const { token } = await this.tokenManager.getToken(); + const entity = await this.catalogClient.getEntityByRef(entityRef, { + token, + }); + const annotationKey = this.useSourceLocation + ? ANNOTATION_SOURCE_LOCATION + : LINGUIST_ANNOTATION; + + let url = entity?.metadata.annotations?.[annotationKey] ?? ''; + if (url.startsWith('url:')) { + url = url.slice(4); + } + + try { + await this.generateEntityLanguages(entityRef, url); + } catch (error) { + assertError(error); + this.logger.error( + `Unable to process "${entityRef}" using "${url}", message: ${error.message}, stack: ${error.stack}`, + ); + } + }); + } + + private async getEntitiesOverview(): Promise<EntitiesOverview> { + this.logger?.debug('Getting pending entities'); + + const processedEntities = await this.store.getProcessedEntities(); + const staleEntities = processedEntities + .filter(pe => { + if (this.age === undefined) return false; + const staleDate = DateTime.now().minus(this.age as HumanDuration); + return DateTime.fromJSDate(pe.processedDate) <= staleDate; + }) + .map(pe => pe.entityRef); + + const unprocessedEntities = await this.store.getUnprocessedEntities(); + const filteredEntities = staleEntities.concat(unprocessedEntities); + + const entitiesOverview: EntitiesOverview = { + entityCount: unprocessedEntities.length, + processedCount: processedEntities.length, + staleCount: staleEntities.length, + pendingCount: filteredEntities.length, + filteredEntities: filteredEntities, + }; + + return entitiesOverview; + } + + private async generateEntityLanguages( + entityRef: string, + url: string, + ): Promise<string> { + this.logger?.info( + `Processing languages for entity ${entityRef} from ${url}`, + ); + + const readTreeResponse = await this.urlReader.readTree(url); + const dir = await readTreeResponse.dir(); + + const results = await linguist(dir); + + try { + const totalBytes = results.languages.bytes; + const langResults = results.languages.results; + + const breakdown: Language[] = []; + for (const key in langResults) { + if (Object.prototype.hasOwnProperty.call(langResults, key)) { + const lang: Language = { + name: key, + percentage: +((langResults[key].bytes / totalBytes) * 100).toFixed( + 2, + ), + bytes: langResults[key].bytes, + type: langResults[key].type, + color: langResults[key].color, + }; + breakdown.push(lang); + } + } + + const languages: Languages = { + languageCount: results.languages.count, + totalBytes: totalBytes, + processedDate: new Date().toISOString(), + breakdown: breakdown, + }; + + const entityResults: EntityResults = { + entityRef: entityRef, + results: languages, + }; + + return await this.store.insertEntityResults(entityResults); + } finally { + this.logger?.info(`Cleaning up files from ${dir}`); + await fs.remove(dir); + } + } +} + +export function kindOrDefault(kind?: string[]) { + if (!kind || kind.length === 0) { + return ['API', 'Component', 'Template']; + } + return kind; +} diff --git a/plugins/scaffolder-backend/src/extension/index.ts b/plugins/linguist-backend/src/api/index.ts similarity index 89% rename from plugins/scaffolder-backend/src/extension/index.ts rename to plugins/linguist-backend/src/api/index.ts index c827d9cafd..a88fa051d4 100644 --- a/plugins/scaffolder-backend/src/extension/index.ts +++ b/plugins/linguist-backend/src/api/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { scaffolderCatalogModule } from './ScaffolderCatalogModule'; + +export { LinguistBackendApi } from './LinguistBackendApi'; diff --git a/plugins/linguist-backend/src/db/LinguistBackendDatabase.ts b/plugins/linguist-backend/src/db/LinguistBackendDatabase.ts new file mode 100644 index 0000000000..cf93374800 --- /dev/null +++ b/plugins/linguist-backend/src/db/LinguistBackendDatabase.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolvePackagePath } from '@backstage/backend-common'; +import { Knex } from 'knex'; +import { v4 as uuid } from 'uuid'; +import { + EntityResults, + Languages, + ProcessedEntity, +} from '@backstage/plugin-linguist-common'; + +export type RawDbEntityResultRow = { + id: string; + entity_ref: string; + languages: string; + processed_date: Date; +}; + +/** @public */ +export interface LinguistBackendStore { + insertEntityResults(entityLanguages: EntityResults): Promise<string>; + insertNewEntity(entityRef: string): Promise<void>; + getEntityResults(entityRef: string): Promise<Languages>; + getProcessedEntities(): Promise<ProcessedEntity[] | []>; + getUnprocessedEntities(): Promise<string[] | []>; +} + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-linguist-backend', + 'migrations', +); + +/** @public */ +export class LinguistBackendDatabase implements LinguistBackendStore { + static async create(knex: Knex): Promise<LinguistBackendStore> { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return new LinguistBackendDatabase(knex); + } + + constructor(private readonly db: Knex) {} + + async insertEntityResults(entityLanguages: EntityResults): Promise<string> { + const entityLanguageId = uuid(); + const entityRef = entityLanguages.entityRef; + + const [result] = await this.db<RawDbEntityResultRow>('entity_result') + .insert({ + id: entityLanguageId, + entity_ref: entityRef, + languages: JSON.stringify(entityLanguages.results), + processed_date: new Date(entityLanguages.results.processedDate), + }) + .onConflict('entity_ref') + .merge(['languages', 'processed_date']) + .returning('id'); + + return result.id; + } + + async insertNewEntity(entityRef: string): Promise<void> { + const entityLanguageId = uuid(); + + await this.db<RawDbEntityResultRow>('entity_result') + .insert({ + id: entityLanguageId, + entity_ref: entityRef, + }) + .onConflict('entity_ref') + .ignore(); // If the entity_ref is in the table already then we don't want to add it again + } + + async getEntityResults(entityRef: string): Promise<Languages> { + const entityResults = await this.db<RawDbEntityResultRow>('entity_result') + .whereNotNull('languages') + .where({ entity_ref: entityRef }) + .first(); + + if (!entityResults) { + const emptyResults: Languages = { + languageCount: 0, + totalBytes: 0, + processedDate: 'undefined', + breakdown: [], + }; + return emptyResults; + } + + try { + return JSON.parse(entityResults.languages); + } catch (error) { + throw new Error(`Failed to parse languages for '${entityRef}', ${error}`); + } + } + + async getProcessedEntities(): Promise<ProcessedEntity[] | []> { + const rawEntities = await this.db<RawDbEntityResultRow>('entity_result') + .whereNotNull('processed_date') + .whereNotNull('languages'); + + if (!rawEntities) { + return []; + } + + const processedEntities = rawEntities.map(rawEntity => { + const processEntity = { + entityRef: rawEntity.entity_ref, + processedDate: rawEntity.processed_date, + }; + + return processEntity; + }); + + return processedEntities; + } + + async getUnprocessedEntities(): Promise<string[] | []> { + const rawEntities = await this.db<RawDbEntityResultRow>('entity_result') + .whereNull('languages') + .orderBy('created_at', 'asc'); + + if (!rawEntities) { + return []; + } + + const unprocessedEntities = rawEntities.map(rawEntity => { + return rawEntity.entity_ref; + }); + + return unprocessedEntities; + } +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/router/paths.ts b/plugins/linguist-backend/src/db/index.ts similarity index 77% rename from plugins/catalog-backend-module-incremental-ingestion/src/router/paths.ts rename to plugins/linguist-backend/src/db/index.ts index 2118177821..c8ec15da68 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/router/paths.ts +++ b/plugins/linguist-backend/src/db/index.ts @@ -14,6 +14,5 @@ * limitations under the License. */ -export const PROVIDER_CLEANUP = '/incremental/cleanup'; -export const PROVIDER_HEALTH = '/incremental/health'; -export const PROVIDER_BASE_PATH = '/incremental/providers/:provider'; +export { LinguistBackendDatabase } from './LinguistBackendDatabase'; +export type { LinguistBackendStore } from './LinguistBackendDatabase'; diff --git a/plugins/linguist-backend/src/index.ts b/plugins/linguist-backend/src/index.ts new file mode 100644 index 0000000000..da1e0ffb00 --- /dev/null +++ b/plugins/linguist-backend/src/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './service/router'; +export { LinguistBackendApi } from './api'; +export { LinguistBackendDatabase } from './db'; +export type { LinguistBackendStore } from './db'; diff --git a/plugins/linguist-backend/src/run.ts b/plugins/linguist-backend/src/run.ts new file mode 100644 index 0000000000..d945aa13f0 --- /dev/null +++ b/plugins/linguist-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/linguist-backend/src/service/router.test.ts b/plugins/linguist-backend/src/service/router.test.ts new file mode 100644 index 0000000000..b8bde9d35d --- /dev/null +++ b/plugins/linguist-backend/src/service/router.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DatabaseManager, + getVoidLogger, + PluginDatabaseManager, + PluginEndpointDiscovery, + TokenManager, + UrlReaders, +} from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { ConfigReader } from '@backstage/config'; +import { createRouter } from './router'; +import { LinguistBackendApi } from '../api'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; + +function createDatabase(): PluginDatabaseManager { + return DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('code-coverage'); +} + +const testDiscovery: jest.Mocked<PluginEndpointDiscovery> = { + getBaseUrl: jest + .fn() + .mockResolvedValue('http://localhost:7007/api/code-coverage'), + getExternalBaseUrl: jest.fn(), +}; + +const mockedTokenManager: jest.Mocked<TokenManager> = { + getToken: jest.fn(), + authenticate: jest.fn(), +}; + +const mockUrlReader = UrlReaders.default({ + logger: getVoidLogger(), + config: new ConfigReader({}), +}); + +const schedule: TaskScheduleDefinition = { + frequency: { minutes: 2 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 15 }, +}; + +describe('createRouter', () => { + let linguistBackendApi: jest.Mocked<LinguistBackendApi>; + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter( + { schedule: schedule, age: { days: 30 }, useSourceLocation: false }, + { + linguistBackendApi, + discovery: testDiscovery, + database: createDatabase(), + reader: mockUrlReader, + logger: getVoidLogger(), + tokenManager: mockedTokenManager, + }, + ); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + 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' }); + }); + }); +}); diff --git a/plugins/linguist-backend/src/service/router.ts b/plugins/linguist-backend/src/service/router.ts new file mode 100644 index 0000000000..66138f5ed2 --- /dev/null +++ b/plugins/linguist-backend/src/service/router.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + errorHandler, + PluginDatabaseManager, + PluginEndpointDiscovery, + TokenManager, + UrlReader, +} from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { LinguistBackendApi } from '../api'; +import { LinguistBackendDatabase } from '../db'; +import { + PluginTaskScheduler, + TaskScheduleDefinition, +} from '@backstage/backend-tasks'; +import { HumanDuration } from '@backstage/types'; + +/** @public */ +export interface PluginOptions { + schedule?: TaskScheduleDefinition; + age?: HumanDuration; + batchSize?: number; + useSourceLocation?: boolean; + kind?: string[]; +} + +/** @public */ +export interface RouterOptions { + linguistBackendApi?: LinguistBackendApi; + logger: Logger; + reader: UrlReader; + tokenManager: TokenManager; + database: PluginDatabaseManager; + discovery: PluginEndpointDiscovery; + scheduler?: PluginTaskScheduler; +} + +/** @public */ +export async function createRouter( + pluginOptions: PluginOptions, + routerOptions: RouterOptions, +): Promise<express.Router> { + const { schedule, age, batchSize, useSourceLocation, kind } = pluginOptions; + + const { logger, reader, database, discovery, scheduler, tokenManager } = + routerOptions; + + const linguistBackendStore = await LinguistBackendDatabase.create( + await database.getClient(), + ); + + const linguistBackendApi = + routerOptions.linguistBackendApi || + new LinguistBackendApi( + logger, + linguistBackendStore, + reader, + discovery, + tokenManager, + age, + batchSize, + useSourceLocation, + kind, + ); + + if (scheduler && schedule) { + logger.info( + `Scheduling processing of entities with: ${JSON.stringify(schedule)}`, + ); + await scheduler.scheduleTask({ + id: 'linguist_process_entities', + frequency: schedule.frequency, + timeout: schedule.timeout, + initialDelay: schedule.initialDelay, + scope: schedule.scope, + fn: async () => { + await linguistBackendApi.processEntities(); + }, + }); + } + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + response.send({ status: 'ok' }); + }); + + /** + * /entity-languages?entity=component:default/my-component + */ + router.get('/entity-languages', async (req, res) => { + const { entityRef: entityRef } = req.query; + + if (!entityRef) { + throw new Error('No entityRef was provided'); + } + + const entityLanguages = await linguistBackendApi.getEntityLanguages( + entityRef as string, + ); + res.status(200).json(entityLanguages); + }); + + router.use(errorHandler()); + return router; +} diff --git a/plugins/linguist-backend/src/service/standaloneServer.ts b/plugins/linguist-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..11604cddfb --- /dev/null +++ b/plugins/linguist-backend/src/service/standaloneServer.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createServiceBuilder, + loadBackendConfig, + SingleHostDiscovery, + UrlReaders, + useHotMemoize, + ServerTokenManager, +} from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; +import knexFactory from 'knex'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise<Server> { + const logger = options.logger.child({ service: 'linguist-backend' }); + const config = await loadBackendConfig({ logger, argv: process.argv }); + const db = useHotMemoize(module, () => { + const knex = knexFactory({ + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }); + + knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => { + resource.run('PRAGMA foreign_keys = ON', () => {}); + }); + + return knex; + }); + + const schedule: TaskScheduleDefinition = { + frequency: { minutes: 2 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 15 }, + }; + + logger.debug('Starting application server...'); + const router = await createRouter( + { schedule: schedule, age: { days: 30 }, useSourceLocation: false }, + { + database: { getClient: async () => db }, + discovery: SingleHostDiscovery.fromConfig(config), + reader: UrlReaders.default({ logger, config }), + logger, + tokenManager: ServerTokenManager.noop(), + }, + ); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/linguist', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/linguist-backend/src/setupTests.ts b/plugins/linguist-backend/src/setupTests.ts new file mode 100644 index 0000000000..813cdeaae3 --- /dev/null +++ b/plugins/linguist-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/linguist-common/.eslintrc.js b/plugins/linguist-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/linguist-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/linguist-common/CHANGELOG.md b/plugins/linguist-common/CHANGELOG.md new file mode 100644 index 0000000000..f2b34faa84 --- /dev/null +++ b/plugins/linguist-common/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/plugin-linguist-common + +## 0.1.0 + +### Minor Changes + +- 75cfee5688: Introduced the Linguist plugin, checkout the plugin's `README.md` for more details! diff --git a/plugins/linguist-common/README.md b/plugins/linguist-common/README.md new file mode 100644 index 0000000000..9408f7fcc5 --- /dev/null +++ b/plugins/linguist-common/README.md @@ -0,0 +1,3 @@ +# Linguist Common + +Common types, permissions, and constants for the Linguist plugin. diff --git a/plugins/linguist-common/api-report.md b/plugins/linguist-common/api-report.md new file mode 100644 index 0000000000..954bef7904 --- /dev/null +++ b/plugins/linguist-common/api-report.md @@ -0,0 +1,48 @@ +## API Report File for "@backstage/plugin-linguist-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public (undocumented) +export type EntitiesOverview = { + entityCount: number; + processedCount: number; + pendingCount: number; + staleCount: number; + filteredEntities: string[]; +}; + +// @public (undocumented) +export type EntityResults = { + entityRef: string; + results: Languages; +}; + +// @public (undocumented) +export type Language = { + name: string; + percentage: number; + bytes: number; + type: string; + color?: `#${string}`; +}; + +// @public (undocumented) +export type Languages = { + languageCount: number; + totalBytes: number; + processedDate: string; + breakdown: Language[]; +}; + +// @public (undocumented) +export const LINGUIST_ANNOTATION = 'backstage.io/linguist'; + +// @public (undocumented) +export type ProcessedEntity = { + entityRef: string; + processedDate: Date; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/linguist-common/package.json b/plugins/linguist-common/package.json new file mode 100644 index 0000000000..c13f40dee2 --- /dev/null +++ b/plugins/linguist-common/package.json @@ -0,0 +1,31 @@ +{ + "name": "@backstage/plugin-linguist-common", + "description": "Common functionalities for the linguist plugin", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/linguist-common/src/constants.ts b/plugins/linguist-common/src/constants.ts new file mode 100644 index 0000000000..55b3d21d02 --- /dev/null +++ b/plugins/linguist-common/src/constants.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. + */ + +/** @public */ +export const LINGUIST_ANNOTATION = 'backstage.io/linguist'; diff --git a/plugins/linguist-common/src/index.ts b/plugins/linguist-common/src/index.ts new file mode 100644 index 0000000000..88fe08c714 --- /dev/null +++ b/plugins/linguist-common/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; +export * from './constants'; diff --git a/plugins/linguist-common/src/setupTests.ts b/plugins/linguist-common/src/setupTests.ts new file mode 100644 index 0000000000..8b9b6bd586 --- /dev/null +++ b/plugins/linguist-common/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/plugins/linguist-common/src/types.ts b/plugins/linguist-common/src/types.ts new file mode 100644 index 0000000000..28a25ed059 --- /dev/null +++ b/plugins/linguist-common/src/types.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @public */ +export type EntityResults = { + entityRef: string; + results: Languages; +}; + +/** @public */ +export type Languages = { + languageCount: number; + totalBytes: number; + processedDate: string; + breakdown: Language[]; +}; + +/** @public */ +export type Language = { + name: string; + percentage: number; + bytes: number; + type: string; + color?: `#${string}`; +}; + +/** @public */ +export type ProcessedEntity = { + entityRef: string; + processedDate: Date; +}; + +/** @public */ +export type EntitiesOverview = { + entityCount: number; + processedCount: number; + pendingCount: number; + staleCount: number; + filteredEntities: string[]; +}; diff --git a/plugins/linguist/.eslintrc.js b/plugins/linguist/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/linguist/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/linguist/CHANGELOG.md b/plugins/linguist/CHANGELOG.md new file mode 100644 index 0000000000..ee28cf7354 --- /dev/null +++ b/plugins/linguist/CHANGELOG.md @@ -0,0 +1,31 @@ +# @backstage/plugin-linguist + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-linguist-common@0.1.0 + +## 0.1.0 + +### Minor Changes + +- 75cfee5688: Introduced the Linguist plugin, checkout the plugin's `README.md` for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-linguist-common@0.1.0 + - @backstage/errors@1.1.4 diff --git a/plugins/linguist/README.md b/plugins/linguist/README.md new file mode 100644 index 0000000000..e7cad1c8e0 --- /dev/null +++ b/plugins/linguist/README.md @@ -0,0 +1,84 @@ +# Linguist + +Welcome to the Linguist plugin! + +## Features + +The Linguist plugin consists of a card that will give you the breakdown of the languages used by the configured Entity in the Catalog. + +When there is no Linguist data yet the card will be empty: + +![Example of empty Linguist card](./docs/linguist-no-data.png) + +Once the language breakdown has been generated the card will look like this: + +![Example of Linguist card without the refresh button](./docs/linguist-with-data.png) + +## Setup + +The following sections will help you get the Linguist plugin setup and running. + +### Backend + +You need to setup the [Linguist backend plugin](../linguist-backend/README.md) before you move forward with any of the following steps if you haven't already. + +### Entity Annotation + +To be able to use the Linguist plugin you need to add the following annotation to any entities you want to use it with: + +```yaml +backstage.io/linguist: https://url.to/your/srouce/code +``` + +Linguist uses the `UrlReader` to pull down the files before it scans them to determine the languages, this means you can pull files from all the supported providers - GitHub, GitLab, Bitbucket, Azure, Google GCS, AWS S3, etc. + +Here's what that will look like in action using the Backstage repo as an example: + +```yaml +# Example catalog-info.yaml entity definition file +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + # ... + annotations: + backstage.io/linguist: https://github.com/backstage/backstage +spec: + type: service + # ... +``` + +### Frontend + +To setup the Linguist Card frontend you'll need to do the following steps: + +1. First we need to add the `@backstage/plugin-linguist` package to your frontend app: + + ```sh + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-linguist + ``` + +2. Second we need to add the `EntityLinguistCard` extension to the entity page in your app: + + ```tsx + // In packages/app/src/components/catalog/EntityPage.tsx + import { isLinguistAvailable, EntityLinguistCard } from '@backstage/plugin-linguist'; + + // For example in the Overview section + const overviewContent = ( + <Grid container spacing={3} alignItems="stretch"> + // ... + <EntitySwitch> + <EntitySwitch.Case if={isLinguistAvailable}> + <Grid item md={6}> + <EntityLinguistCard /> + </Grid> + </EntitySwitch.Case> + </EntitySwitch> + // ... + </Grid> + ``` + +**Notes:** + +- The `if` prop is optional on the `EntitySwitch.Case`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation diff --git a/plugins/linguist/api-report.md b/plugins/linguist/api-report.md new file mode 100644 index 0000000000..469b3b93c0 --- /dev/null +++ b/plugins/linguist/api-report.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/plugin-linguist" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="react" /> + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; + +// @public (undocumented) +export const EntityLinguistCard: () => JSX.Element; + +// @public (undocumented) +export const isLinguistAvailable: (entity: Entity) => boolean; + +// @public (undocumented) +export const linguistPlugin: BackstagePlugin<{}, {}, {}>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/linguist/dev/index.tsx b/plugins/linguist/dev/index.tsx new file mode 100644 index 0000000000..bb6fb64b52 --- /dev/null +++ b/plugins/linguist/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { linguistPlugin, EntityLinguistCard } from '../src/plugin'; + +createDevApp() + .registerPlugin(linguistPlugin) + .addPage({ + element: <EntityLinguistCard />, + title: 'Root Page', + path: '/linguist', + }) + .render(); diff --git a/plugins/linguist/docs/linguist-no-data.png b/plugins/linguist/docs/linguist-no-data.png new file mode 100644 index 0000000000..07661fe02d Binary files /dev/null and b/plugins/linguist/docs/linguist-no-data.png differ diff --git a/plugins/linguist/docs/linguist-with-data.png b/plugins/linguist/docs/linguist-with-data.png new file mode 100644 index 0000000000..a3b4e4fde7 Binary files /dev/null and b/plugins/linguist/docs/linguist-with-data.png differ diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json new file mode 100644 index 0000000000..0b234878b1 --- /dev/null +++ b/plugins/linguist/package.json @@ -0,0 +1,63 @@ +{ + "name": "@backstage/plugin-linguist", + "version": "0.1.1-next.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/linguist" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-linguist-common": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "luxon": "^2.0.2", + "react-use": "^17.2.4", + "slugify": "^1.6.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/linguist/src/api/LinguistApi.ts b/plugins/linguist/src/api/LinguistApi.ts new file mode 100644 index 0000000000..6593453f31 --- /dev/null +++ b/plugins/linguist/src/api/LinguistApi.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core-plugin-api'; +import { Languages } from '@backstage/plugin-linguist-common'; + +export const linguistApiRef = createApiRef<LinguistApi>({ + id: 'plugin.linguist.service', +}); + +export interface LinguistApi { + getLanguages(entityRef: string): Promise<Languages>; +} diff --git a/plugins/linguist/src/api/LinguistClient.ts b/plugins/linguist/src/api/LinguistClient.ts new file mode 100644 index 0000000000..3e327ff21e --- /dev/null +++ b/plugins/linguist/src/api/LinguistClient.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 { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { Languages } from '@backstage/plugin-linguist-common'; +import { LinguistApi } from './LinguistApi'; + +export class LinguistClient implements LinguistApi { + private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; + + public constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { + this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; + } + + public async getLanguages(entityRef: string): Promise<Languages> { + const queryString = new URLSearchParams(); + queryString.append('entityRef', entityRef); + + const urlSegment = `entity-languages?${queryString}`; + + const items = await this.get<Languages>(urlSegment); + return items; + } + + private async get<T>(path: string): Promise<T> { + const baseUrl = `${await this.discoveryApi.getBaseUrl('linguist')}/`; + const url = new URL(path, baseUrl); + + const { token } = await this.identityApi.getCredentials(); + const response = await fetch(url.toString(), { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response.json() as Promise<T>; + } +} diff --git a/plugins/linguist/src/api/index.ts b/plugins/linguist/src/api/index.ts new file mode 100644 index 0000000000..ca60b60d53 --- /dev/null +++ b/plugins/linguist/src/api/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 './LinguistApi'; +export * from './LinguistClient'; diff --git a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx new file mode 100644 index 0000000000..6014487d30 --- /dev/null +++ b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx @@ -0,0 +1,153 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Box, + Chip, + Tooltip, + Typography, + makeStyles, + Grid, + useTheme, +} from '@material-ui/core'; +import { InfoCard, Progress } from '@backstage/core-components'; +import Alert from '@material-ui/lab/Alert'; +import { DateTime } from 'luxon'; +import React from 'react'; +import slugify from 'slugify'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { useLanguages } from '../../hooks'; + +const useStyles = makeStyles(theme => ({ + infoCard: { + marginBottom: theme.spacing(3), + }, + barContainer: { + height: theme.spacing(2), + marginBottom: theme.spacing(3), + borderRadius: '4px', + backgroundColor: 'transparent', + overflow: 'hidden', + }, + bar: { + height: '100%', + position: 'relative', + }, + languageDot: { + width: '10px', + height: '10px', + borderRadius: '50%', + marginRight: theme.spacing(1), + display: 'inline-block', + }, + label: { + color: 'inherit', + }, +})); + +export const LinguistCard = () => { + const classes = useStyles(); + const theme = useTheme(); + const { entity } = useEntity(); + const { items, loading, error } = useLanguages(entity); + let barWidth = 0; + + if (loading) { + return <Progress />; + } else if (error) { + return <Alert severity="error">{error.message}</Alert>; + } + + if (items && items.languageCount === 0 && items.totalBytes === 0) { + return ( + <InfoCard title="Languages" className={classes.infoCard}> + <Grid container spacing={3}> + <Box p={2}> + <Typography> + There is currently no language data for this entity. + </Typography> + </Box> + </Grid> + </InfoCard> + ); + } + + const breakdown = items?.breakdown.sort((a, b) => + a.percentage < b.percentage ? 1 : -1, + ); + const processedDate = items?.processedDate; + + return breakdown && processedDate ? ( + <InfoCard title="Languages" className={classes.infoCard}> + <Box className={classes.barContainer}> + {breakdown.map((language, index: number) => { + barWidth = barWidth + language.percentage; + return ( + <Tooltip + title={language.name} + placement="bottom-end" + key={slugify(language.name, { lower: true })} + > + <Box + className={classes.bar} + key={slugify(language.name, { lower: true })} + style={{ + marginTop: index === 0 ? '0' : `-16px`, + zIndex: Object.keys(breakdown).length - index, + backgroundColor: + language.color?.toString() || + theme.palette.background.default, + width: `${barWidth}%`, + }} + /> + </Tooltip> + ); + })} + </Box> + <Tooltip + title={`Generated ${DateTime.fromISO(processedDate).toRelative()}`} + > + <Box> + {breakdown.map(languages => ( + <Chip + classes={{ + label: classes.label, + }} + label={ + <Box> + <Box + component="span" + className={classes.languageDot} + style={{ + backgroundColor: + languages?.color?.toString() || + theme.palette.background.default, + }} + /> + {languages.name} - {languages.percentage}% + </Box> + } + variant="outlined" + key={slugify(languages.name, { lower: true })} + /> + ))} + </Box> + </Tooltip> + </InfoCard> + ) : ( + <></> + ); +}; diff --git a/plugins/linguist/src/components/LinguistCard/index.ts b/plugins/linguist/src/components/LinguistCard/index.ts new file mode 100644 index 0000000000..aefae143fd --- /dev/null +++ b/plugins/linguist/src/components/LinguistCard/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 { LinguistCard } from './LinguistCard'; diff --git a/plugins/linguist/src/hooks/index.ts b/plugins/linguist/src/hooks/index.ts new file mode 100644 index 0000000000..6f8913cf35 --- /dev/null +++ b/plugins/linguist/src/hooks/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 './useLanguages'; diff --git a/plugins/linguist/src/hooks/useLanguages.ts b/plugins/linguist/src/hooks/useLanguages.ts new file mode 100644 index 0000000000..3e99243e94 --- /dev/null +++ b/plugins/linguist/src/hooks/useLanguages.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { linguistApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; + +import { Languages } from '@backstage/plugin-linguist-common'; + +export function useLanguages(entity: Entity): { + items?: Languages; + loading: boolean; + error?: Error; +} { + const entityRef = stringifyEntityRef(entity); + + const api = useApi(linguistApiRef); + const { value, loading, error } = useAsync(() => { + return api.getLanguages(entityRef); + }, [api, entityRef]); + + return { + items: value, + loading, + error, + }; +} diff --git a/plugins/linguist/src/index.ts b/plugins/linguist/src/index.ts new file mode 100644 index 0000000000..ce9cd98cbd --- /dev/null +++ b/plugins/linguist/src/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + linguistPlugin, + isLinguistAvailable, + EntityLinguistCard, +} from './plugin'; diff --git a/plugins/linguist/src/plugin.test.ts b/plugins/linguist/src/plugin.test.ts new file mode 100644 index 0000000000..d2a1016a22 --- /dev/null +++ b/plugins/linguist/src/plugin.test.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 { linguistPlugin } from './plugin'; + +describe('linguist', () => { + it('should export plugin', () => { + expect(linguistPlugin).toBeDefined(); + }); +}); diff --git a/plugins/linguist/src/plugin.ts b/plugins/linguist/src/plugin.ts new file mode 100644 index 0000000000..917a13ca9a --- /dev/null +++ b/plugins/linguist/src/plugin.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createApiFactory, + createComponentExtension, + createPlugin, + discoveryApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { linguistApiRef, LinguistClient } from './api'; +import { LINGUIST_ANNOTATION } from '@backstage/plugin-linguist-common'; +import { Entity } from '@backstage/catalog-model'; + +/** @public */ +export const isLinguistAvailable = (entity: Entity) => + Boolean(entity.metadata.annotations?.[LINGUIST_ANNOTATION]); + +/** @public */ +export const linguistPlugin = createPlugin({ + id: 'linguist', + apis: [ + createApiFactory({ + api: linguistApiRef, + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new LinguistClient({ discoveryApi, identityApi }), + }), + ], +}); + +/** @public */ +export const EntityLinguistCard = linguistPlugin.provide( + createComponentExtension({ + name: 'EntityLinguistCard', + component: { + lazy: () => import('./components/LinguistCard').then(m => m.LinguistCard), + }, + }), +); diff --git a/plugins/linguist/src/setupTests.ts b/plugins/linguist/src/setupTests.ts new file mode 100644 index 0000000000..2b5b615b19 --- /dev/null +++ b/plugins/linguist/src/setupTests.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. + */ + +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/microsoft-calendar/.eslintrc.js b/plugins/microsoft-calendar/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/microsoft-calendar/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/microsoft-calendar/CHANGELOG.md b/plugins/microsoft-calendar/CHANGELOG.md new file mode 100644 index 0000000000..c9e1da8fb1 --- /dev/null +++ b/plugins/microsoft-calendar/CHANGELOG.md @@ -0,0 +1,28 @@ +# @backstage/plugin-microsoft-calendar + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.1.0 + +### Minor Changes + +- 44412375bb: Created a new plugin `@backstage/plugin-microsoft-calendar` to display events from a Microsoft Calendar. + + please refer to the [README.md](https://github.com/backstage/backstage/blob/master/plugins/microsoft-calendar/README.md) for step-by-step instructions to setup the plugin in your Backstage instance. + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/errors@1.1.4 diff --git a/plugins/microsoft-calendar/README.md b/plugins/microsoft-calendar/README.md new file mode 100644 index 0000000000..219a7cf2dc --- /dev/null +++ b/plugins/microsoft-calendar/README.md @@ -0,0 +1,51 @@ +# Microsoft-Calendar Plugin + +Welcome to the Microsoft calendar plugin! +_This plugin was created through the Backstage CLI_ + +## Features + +1. You can switch between calendars, using the select menu on the calendar card header. +2. Card showing the list of events on the selected date and the selected calendar (provided by Outlook calendar). +3. Link to join the online meeting on the event card if provided. so you can join your meetings right away hassle-free. +4. Hovering over the event will pop over a card showing the event summary message, and list of attendees. +5. attendee's chips will have a badge over them symbolizing their responses. + - green --> accepted + - red --> declined + - nothing --> not responded yet + +## Setup + +The following sections will help you set up the Microsoft calendar plugin. + +### Microsoft azure authentication provider + +> You need to setup [Microsoft Azure authentication provider](https://backstage.io/docs/auth/microsoft/provider), before you move forward with any of the below step if you haven't already. + +1. Install the plugin by running this command + +```bash +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-microsoft-calendar +``` + +2. Import the Microsoft calendar React component from `@backstage/plugin-microsoft-calendar`. +3. You can then use the provided React component `MicrosoftCalendar` in the backstage frontend where ever you want + +```tsx +import { MicrosoftCalendar } from '@backstage/plugin-microsoft-calendar'; + +// ... +<Grid item xs={12} md={4}> + <MicrosoftCalendar /> +</Grid>; +// ... +``` + +![Microsoft Calendar plugin demo](https://user-images.githubusercontent.com/23618736/215717491-25db5fa6-b237-487f-8c00-28f572e8da05.mp4) + +![Sample](./docs/microsoft-calendar-plugin.png) + +You can also 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. diff --git a/plugins/microsoft-calendar/api-report.md b/plugins/microsoft-calendar/api-report.md new file mode 100644 index 0000000000..d6839f19ea --- /dev/null +++ b/plugins/microsoft-calendar/api-report.md @@ -0,0 +1,39 @@ +## API Report File for "@backstage/plugin-microsoft-calendar" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="react" /> + +import { ApiRef } from '@backstage/core-plugin-api'; +import { Calendar } from '@microsoft/microsoft-graph-types'; +import { Event as Event_2 } from '@microsoft/microsoft-graph-types'; +import { FetchApi } from '@backstage/core-plugin-api'; +import { OAuthApi } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export class MicrosoftCalendarApiClient { + constructor(options: { authApi: OAuthApi; fetchApi: FetchApi }); + // (undocumented) + getCalendars(): Promise<Calendar[]>; + // (undocumented) + getEvents( + calendarId: string, + params: { + startDateTime: string; + endDateTime: string; + }, + headers: { + [key in string]: any; + }, + ): Promise<Event_2[]>; +} + +// @public (undocumented) +export const microsoftCalendarApiRef: ApiRef<MicrosoftCalendarApiClient>; + +// @public (undocumented) +export const MicrosoftCalendarCard: () => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/microsoft-calendar/dev/index.tsx b/plugins/microsoft-calendar/dev/index.tsx new file mode 100644 index 0000000000..d611bca20c --- /dev/null +++ b/plugins/microsoft-calendar/dev/index.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { microsoftCalendarPlugin, MicrosoftCalendarCard } from '../src/plugin'; +import { microsoftCalendarApiRef } from '../src'; +import responseMock from './mock.json'; +import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; +import { Content, Page } from '@backstage/core-components'; +import { Grid } from '@material-ui/core'; + +createDevApp() + .registerPlugin(microsoftCalendarPlugin) + .registerApi({ + api: microsoftAuthApiRef, + deps: {}, + factory: () => + ({ + async getAccessToken() { + return Promise.resolve('token'); + }, + } as unknown as typeof microsoftAuthApiRef.T), + }) + .registerApi({ + api: microsoftCalendarApiRef, + deps: {}, + factory: () => + ({ + async getCalendars() { + return Promise.resolve(responseMock.calendars); + }, + async getEvents() { + return Promise.resolve(responseMock.events); + }, + } as unknown as typeof microsoftCalendarApiRef.T), + }) + .addPage({ + element: ( + <Page themeId="home"> + <Content> + <Grid item xs={12} md={6}> + <MicrosoftCalendarCard /> + </Grid> + </Content> + </Page> + ), + title: 'Microsoft-Calendar Plugin Demo', + path: '/microsoft-calendar', + }) + .render(); diff --git a/plugins/microsoft-calendar/dev/mock.json b/plugins/microsoft-calendar/dev/mock.json new file mode 100644 index 0000000000..b305e187eb --- /dev/null +++ b/plugins/microsoft-calendar/dev/mock.json @@ -0,0 +1,305 @@ +{ + "calendars": [ + { + "id": "AQMkADQyMDdlODU1AC05OGNiLTRhZmItOGM2Mi1kODZhMzJkYWU2OTAARgAAAwWXy9axQ31DtBuHDIFwgtYHAKRSO4MUJXZDi1hwzpYLetAAAAIBBgAAAKRSO4MUJXZDi1hwzpYLetAAAAIyswAAAA==", + "name": "Calendar", + "color": "lightBlue", + "hexColor": "#9fe1e7", + "isDefaultCalendar": true, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAAAADYw==", + "canShare": true, + "canViewPrivateItems": true, + "canEdit": true, + "allowedOnlineMeetingProviders": ["teamsForBusiness"], + "defaultOnlineMeetingProvider": "teamsForBusiness", + "isTallyingResponses": true, + "isRemovable": false, + "owner": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAEGAACkUjuDFCV2Q4tYcM6WC3rQAAAZs65BAAA=", + "name": "United Kingdom holidays", + "color": "auto", + "hexColor": "", + "isDefaultCalendar": false, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAGanqjg==", + "canShare": false, + "canViewPrivateItems": true, + "canEdit": false, + "allowedOnlineMeetingProviders": [], + "defaultOnlineMeetingProvider": "unknown", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAEGAACkUjuDFCV2Q4tYcM6WC3rQAAAZs65CAAA=", + "name": "Birthdays1", + "color": "auto", + "hexColor": "", + "isDefaultCalendar": false, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAGanrfQ==", + "canShare": false, + "canViewPrivateItems": true, + "canEdit": false, + "allowedOnlineMeetingProviders": [], + "defaultOnlineMeetingProvider": "unknown", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + { + "id": "AQMkADQyMDdlODU1AC05OGNiLTRhZmItOGM2Mi1kODZhMzJkYWU2OTAARgAAAwWXy9axQ31DtBuHDIFwgtYHAKRSO4MUJXZDi1hwzpYLetAAAAIBBgAAAKRSO4MUJXZDi1hwzpYLetAAAAIytQAAAA==", + "name": "Birthdays", + "color": "auto", + "hexColor": "#92e1c0", + "isDefaultCalendar": false, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAAAADdw==", + "canShare": false, + "canViewPrivateItems": true, + "canEdit": false, + "allowedOnlineMeetingProviders": [], + "defaultOnlineMeetingProvider": "unknown", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + { + "id": "AQMkADQyMDdlODU1AC05OGNiLTRhZmItOGM2Mi1kODZhMzJkYWU2OTAARgAAAwWXy9axQ31DtBuHDIFwgtYHAKRSO4MUJXZDi1hwzpYLetAAAAIBBgAAAKRSO4MUJXZDi1hwzpYLetAAAAIytgAAAA==", + "name": "Holidays in India", + "color": "auto", + "hexColor": "#16a765", + "isDefaultCalendar": false, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAAAADug==", + "canShare": false, + "canViewPrivateItems": true, + "canEdit": false, + "allowedOnlineMeetingProviders": [], + "defaultOnlineMeetingProvider": "unknown", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "Abhay Soni", + "address": "abhay.s@statusneo.com" + } + } + ], + "events": [ + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", + "createdDateTime": "2022-12-22T08:28:36.6131501Z", + "lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z", + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", + "categories": [], + "transactionId": null, + "originalStartTimeZone": "India Standard Time", + "originalEndTimeZone": "India Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "Abhay Soni", + "bodyPreview": "", + "importance": "normal", + "sensitivity": "normal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": false, + "responseRequested": true, + "seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", + "showAs": "tentative", + "type": "occurrence", + "webLink": "", + "onlineMeetingUrl": "", + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "notResponded", + "time": "0001-01-01T00:00:00Z" + }, + "body": { + "contentType": "html", + "content": "" + }, + "start": { + "dateTime": "2023-01-16T11:00:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "end": { + "dateTime": "2023-01-16T11:30:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "attendees": [ + { + "type": "required", + "status": { + "response": "none", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Nishkarsh Raj", + "address": "nishkarshraj@nishkarsh.com" + } + }, + { + "type": "required", + "status": { + "response": "accepted", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Hakam Ram", + "address": "hakam_ram@ram.com" + } + }, + { + "type": "required", + "status": { + "response": "declined", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + } + ], + "organizer": { + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://abhay-soni-developer.github.io/MyReusme/" + } + }, + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", + "createdDateTime": "2022-12-22T08:28:36.6131501Z", + "lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z", + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", + "categories": [], + "transactionId": null, + "originalStartTimeZone": "India Standard Time", + "originalEndTimeZone": "India Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "https://abhay-soni-developer.github.io/MyReusme/", + "bodyPreview": "", + "importance": "normal", + "sensitivity": "normal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": false, + "responseRequested": true, + "seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", + "showAs": "tentative", + "type": "occurrence", + "webLink": "", + "onlineMeetingUrl": "", + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "notResponded", + "time": "0001-01-01T00:00:00Z" + }, + "attendees": [ + { + "type": "required", + "status": { + "response": "none", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Nishkarsh Raj", + "address": "nishkarshraj@nishkarsh.com" + } + }, + { + "type": "required", + "status": { + "response": "accepted", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Hakam Ram", + "address": "hakam_ram@ram.com" + } + }, + { + "type": "required", + "status": { + "response": "declined", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + } + ], + "body": { + "contentType": "html", + "content": "" + }, + "start": { + "dateTime": "2023-01-16T11:00:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "end": { + "dateTime": "2023-01-16T11:30:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "organizer": { + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://abhay-soni-developer.github.io/MyReusme/" + } + } + ] +} diff --git a/plugins/microsoft-calendar/docs/microsoft-calendar-plugin.png b/plugins/microsoft-calendar/docs/microsoft-calendar-plugin.png new file mode 100644 index 0000000000..376ae73717 Binary files /dev/null and b/plugins/microsoft-calendar/docs/microsoft-calendar-plugin.png differ diff --git a/plugins/microsoft-calendar/package.json b/plugins/microsoft-calendar/package.json new file mode 100644 index 0000000000..ce2f148d6c --- /dev/null +++ b/plugins/microsoft-calendar/package.json @@ -0,0 +1,63 @@ +{ + "name": "@backstage/plugin-microsoft-calendar", + "version": "0.1.1-next.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "author": { + "name": "Abhay Soni", + "email": "abhaysoni.developer@gmail.com", + "url": "https://github.com/Abhay-soni-developer" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@microsoft/microsoft-graph-types": "^2.25.0", + "@tanstack/react-query": "^4.1.3", + "classnames": "^2.3.1", + "dompurify": "^2.3.6", + "lodash": "^4.17.21", + "luxon": "^3.0.0", + "material-ui-popup-state": "^1.9.3", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "^16.11.26", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/microsoft-calendar/src/api/client.ts b/plugins/microsoft-calendar/src/api/client.ts new file mode 100644 index 0000000000..fe842b5aef --- /dev/null +++ b/plugins/microsoft-calendar/src/api/client.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OAuthApi, createApiRef, FetchApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { MicrosoftCalendarEvent, MicrosoftCalendar } from './types'; + +/** @public */ +export const microsoftCalendarApiRef = createApiRef<MicrosoftCalendarApiClient>( + { + id: 'plugin.microsoft-calendar.service', + }, +); + +/** @public */ +export class MicrosoftCalendarApiClient { + private readonly authApi: OAuthApi; + private readonly fetchApi: FetchApi; + + constructor(options: { authApi: OAuthApi; fetchApi: FetchApi }) { + this.authApi = options.authApi; + this.fetchApi = options.fetchApi; + } + + private async get<T>( + path: string, + params: { [key in string]: any } = {}, + headers?: any, + ): Promise<T> { + const query = new URLSearchParams(params); + const url = new URL( + `${path}?${query.toString()}`, + 'https://graph.microsoft.com', + ); + const token = await this.authApi.getAccessToken(); + let temp: any = {}; + + if (headers && typeof headers === 'object') { + temp = { + ...headers, + }; + } + + if (token) { + temp.Authorization = `Bearer ${token}`; + } + + const response = await this.fetchApi.fetch(url.toString(), { + headers: temp, + }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response.json() as Promise<T>; + } + + public async getCalendars(): Promise<MicrosoftCalendar[]> { + const data = await this.get<{ + id: string; + value: MicrosoftCalendar[]; + }>('v1.0/me/calendars'); + + return data.value; + } + + public async getEvents( + calendarId: string, + params: { + startDateTime: string; + endDateTime: string; + }, + headers: { [key in string]: any }, + ): Promise<MicrosoftCalendarEvent[]> { + const data = await this.get<{ + id: string; + value: [MicrosoftCalendarEvent]; + }>(`v1.0/me/calendars/${calendarId}/calendarview`, params, headers); + return data.value; + } +} diff --git a/plugins/microsoft-calendar/src/api/index.ts b/plugins/microsoft-calendar/src/api/index.ts new file mode 100644 index 0000000000..db4616b586 --- /dev/null +++ b/plugins/microsoft-calendar/src/api/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './client'; +export * from './types'; diff --git a/plugins/microsoft-calendar/src/api/types.ts b/plugins/microsoft-calendar/src/api/types.ts new file mode 100644 index 0000000000..f4cb2811c7 --- /dev/null +++ b/plugins/microsoft-calendar/src/api/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Calendar as MicrosoftCalendar, + Event as MicrosoftCalendarEvent, + Attendee, + ResponseStatus, + DateTimeTimeZone, + EmailAddress, +} from '@microsoft/microsoft-graph-types'; + +export type { + MicrosoftCalendar, + MicrosoftCalendarEvent, + Attendee, + ResponseStatus, + DateTimeTimeZone, + EmailAddress, +}; + +export const ResponseStatusMap = { + needsAction: 'none', + accepted: 'accepted', + declined: 'declined', + maybe: 'tentativelyAccepted', + notResponded: 'notResponded', +}; diff --git a/plugins/microsoft-calendar/src/components/AttendeeChip.tsx b/plugins/microsoft-calendar/src/components/AttendeeChip.tsx new file mode 100644 index 0000000000..67dd601da7 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/AttendeeChip.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { BackstageTheme } from '@backstage/theme'; + +import { Badge, Chip, makeStyles } from '@material-ui/core'; +import CancelIcon from '@material-ui/icons/Cancel'; +import CheckIcon from '@material-ui/icons/CheckCircle'; + +import { Attendee, ResponseStatusMap } from '../api'; + +const useStyles = makeStyles((theme: BackstageTheme) => { + const getIconColor = (responseStatus?: string) => { + if (!responseStatus) return theme.palette.primary.light; + + return { + [ResponseStatusMap.accepted]: theme.palette.status.ok, + [ResponseStatusMap.declined]: theme.palette.status.error, + }[responseStatus]; + }; + + return { + responseStatus: { + color: ({ responseStatus }: { responseStatus?: string }) => + getIconColor(responseStatus), + }, + badge: { + right: 10, + top: 5, + '& svg': { + height: 16, + width: 16, + background: '#fff', + }, + }, + }; +}); + +const ResponseIcon = ({ responseStatus }: { responseStatus: string }) => { + if (responseStatus === ResponseStatusMap.accepted) { + return <CheckIcon data-testid="accepted-icon" />; + } + if (responseStatus === ResponseStatusMap.declined) { + return <CancelIcon data-testid="declined-icon" />; + } + + return null; +}; + +type AttendeeChipProps = { + user: Attendee; +}; + +export const AttendeeChip = ({ user }: AttendeeChipProps) => { + const classes = useStyles({ responseStatus: user.status?.response || '' }); + + return ( + <Badge + classes={{ + root: classes.responseStatus, + badge: classes.badge, + }} + badgeContent={ + <ResponseIcon responseStatus={user.status?.response || ''} /> + } + > + <Chip + size="small" + variant="outlined" + label={user.emailAddress?.address} + color="primary" + /> + </Badge> + ); +}; diff --git a/plugins/microsoft-calendar/src/components/CalendarCard.tsx b/plugins/microsoft-calendar/src/components/CalendarCard.tsx new file mode 100644 index 0000000000..dd94bad148 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/CalendarCard.tsx @@ -0,0 +1,141 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { sortBy } from 'lodash'; +import { DateTime } from 'luxon'; +import React, { useState } from 'react'; + +import { InfoCard, Progress } from '@backstage/core-components'; + +import { Box, IconButton, Typography } from '@material-ui/core'; +import PrevIcon from '@material-ui/icons/NavigateBefore'; +import NextIcon from '@material-ui/icons/NavigateNext'; + +import { useCalendarsQuery, useEventsQuery, useSignIn } from '../hooks'; +import calendarIcon from '../icons/calendar.svg'; +import { CalendarEvent } from './CalendarEvent'; +import { CalendarSelect } from './CalendarSelect'; +import { SignInContent } from './SignInContent'; +import { getStartDate } from './util'; +import useAsync from 'react-use/lib/useAsync'; + +export const CalendarCard = () => { + const [date, setDate] = useState(DateTime.now()); + const [selectedCalendarId, setSelectedCalendarId] = useState(''); + + const changeDay = (offset = 1) => { + setDate(prev => prev.plus({ day: offset })); + }; + + const { isSignedIn, isInitialized, signIn } = useSignIn(); + + useAsync(async () => signIn(true), [signIn]); + + const { + isLoading: isCalendarLoading, + isFetching: isCalendarFetching, + data: calendars = [], + } = useCalendarsQuery({ + enabled: isSignedIn, + }); + + const defaultCalendarId = calendars.find(c => c.isDefaultCalendar)?.id; + + const { data: events, isLoading: isEventLoading } = useEventsQuery({ + calendarId: selectedCalendarId || defaultCalendarId || '', + enabled: isSignedIn && calendars.length > 0, + timeMin: date.startOf('day').toISO(), + timeMax: date.endOf('day').toISO(), + timeZone: date.zoneName, + }); + + const showLoader = + (isCalendarLoading && isCalendarFetching) || + isEventLoading || + !isInitialized; + + return ( + <InfoCard + noPadding + title={ + <Box display="flex" alignItems="center"> + <Box height={32} width={32} mr={1}> + <img src={calendarIcon} alt="Microsoft Calendar" /> + </Box> + {isSignedIn ? ( + <> + <IconButton onClick={() => changeDay(-1)} size="small"> + <PrevIcon /> + </IconButton> + <IconButton onClick={() => changeDay(1)} size="small"> + <NextIcon /> + </IconButton> + <Box mr={0.5} /> + <Typography variant="h6"> + {date.toLocaleString({ + weekday: 'short', + month: 'short', + day: 'numeric', + })} + </Typography> + + <Box flex={1} /> + + <CalendarSelect + calendars={calendars} + selectedCalendarId={selectedCalendarId || defaultCalendarId} + setSelectedCalendarId={setSelectedCalendarId} + disabled={ + (isCalendarFetching && isCalendarLoading) || !isSignedIn + } + /> + </> + ) : ( + <Typography variant="h6">Agenda</Typography> + )} + </Box> + } + deepLink={{ + link: 'https://outlook.office.com/calendar/', + title: 'Go to Calendar', + }} + > + <Box> + {showLoader && ( + <Box py={2}> + <Progress variant="query" /> + </Box> + )} + {!isSignedIn && isInitialized && ( + <SignInContent handleAuthClick={() => signIn(false)} /> + )} + {!isEventLoading && !isCalendarLoading && isSignedIn && ( + <Box p={1} pb={0} maxHeight={602} overflow="auto"> + {events?.length === 0 && ( + <Box pt={2} pb={2}> + <Typography align="center" variant="h6"> + No events + </Typography> + </Box> + )} + {sortBy(events, [getStartDate]).map(event => ( + <CalendarEvent key={`${event.id}`} event={event} /> + ))} + </Box> + )} + </Box> + </InfoCard> + ); +}; diff --git a/plugins/microsoft-calendar/src/components/CalendarEvent.tsx b/plugins/microsoft-calendar/src/components/CalendarEvent.tsx new file mode 100644 index 0000000000..e56e25a09b --- /dev/null +++ b/plugins/microsoft-calendar/src/components/CalendarEvent.tsx @@ -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 classnames from 'classnames'; +import { + bindPopover, + bindTrigger, + usePopupState, +} from 'material-ui-popup-state/hooks'; +import React, { useState } from 'react'; + +import { Link } from '@backstage/core-components'; + +import { + Box, + Paper, + Popover, + Tooltip, + Typography, + makeStyles, +} from '@material-ui/core'; + +import webcamIcon from '../icons/webcam.svg'; +import { CalendarEventPopoverContent } from './CalendarEventPopoverContent'; +import { MicrosoftCalendarEvent } from '../api'; +import { + getOnlineMeetingLink, + getTimePeriod, + isAllDay, + isPassed, +} from './util'; + +const useStyles = makeStyles( + theme => ({ + event: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(1), + cursor: 'pointer', + paddingRight: 12, + }, + declined: { + textDecoration: 'line-through', + }, + passed: { + opacity: 0.6, + transition: 'opacity 0.15s ease-in-out', + '&:hover': { + opacity: 1, + }, + }, + link: { + width: 48, + height: 48, + display: 'inline-block', + padding: 8, + borderRadius: '50%', + '&:hover': { + backgroundColor: theme.palette.grey[100], + }, + }, + calendarColor: { + width: 8, + borderTopLeftRadius: 4, + borderBottomLeftRadius: 4, + }, + }), + { + name: 'MicrosoftCalendarEvent', + }, +); + +export const CalendarEvent = ({ event }: { event: MicrosoftCalendarEvent }) => { + const classes = useStyles(); + const popoverState = usePopupState({ + variant: 'popover', + popupId: event.id, + disableAutoFocus: true, + }); + const [hovered, setHovered] = useState(false); + const onlineMeetingLink = getOnlineMeetingLink(event); + + const { onClick, ...restBindProps } = bindTrigger(popoverState); + + return ( + <> + <Paper + onClick={e => { + onClick(e); + }} + {...restBindProps} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + elevation={hovered ? 4 : 1} + className={classnames(classes.event, { + [classes.passed]: isPassed(event), + })} + data-testid="microsoft-calendar-event" + > + <Box className={classes.calendarColor} mr={1} alignSelf="stretch" /> + <Box flex={1} pt={1} pb={1}> + <Typography + variant="subtitle2" + className={classnames({ [classes.declined]: event.isCancelled })} + > + {event.subject} + </Typography> + {!isAllDay(event) && ( + <Typography variant="body2" data-testid="calendar-event-time"> + {getTimePeriod(event)} + </Typography> + )} + </Box> + + {event.isOnlineMeeting && ( + <Tooltip title="Join Online Meeting"> + <Link + data-testid="calendar-event-online-meeting-link" + className={classes.link} + to={onlineMeetingLink} + onClick={e => { + e.stopPropagation(); + }} + noTrack + > + {/* we can use onlineMeetingProvider to show icon accordingly */} + <img + height={32} + width={32} + src={webcamIcon} + alt="Online Meeting link" + /> + </Link> + </Tooltip> + )} + </Paper> + + <Popover + {...bindPopover(popoverState)} + anchorOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + data-testid="calendar-event-popover" + > + <CalendarEventPopoverContent event={event} /> + </Popover> + </> + ); +}; diff --git a/plugins/microsoft-calendar/src/components/CalendarEventPopoverContent.tsx b/plugins/microsoft-calendar/src/components/CalendarEventPopoverContent.tsx new file mode 100644 index 0000000000..715a1ad276 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/CalendarEventPopoverContent.tsx @@ -0,0 +1,126 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { sortBy } from 'lodash'; +import React from 'react'; +import DOMPurify from 'dompurify'; + +import { Link } from '@backstage/core-components'; +import { + Box, + Divider, + IconButton, + Tooltip, + Typography, + makeStyles, +} from '@material-ui/core'; +import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; + +import { AttendeeChip } from './AttendeeChip'; +import { MicrosoftCalendarEvent } from '../api'; +import { getTimePeriod, getOnlineMeetingLink } from './util'; + +const useStyles = makeStyles( + theme => ({ + description: { + wordBreak: 'break-word', + '& a': { + color: theme.palette.primary.main, + fontWeight: 500, + }, + }, + divider: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + }), + { + name: 'MicrosoftCalendarEventPopoverContent', + }, +); + +type CalendarEventPopoverProps = { + event: MicrosoftCalendarEvent; +}; + +export const CalendarEventPopoverContent = ({ + event, +}: CalendarEventPopoverProps) => { + const classes = useStyles(); + const onlineMeetingLink = getOnlineMeetingLink(event); + + return ( + <Box display="flex" flexDirection="column" width={400} p={2}> + <Box display="flex" alignItems="center"> + <Box flex={1}> + <Typography variant="h6">{event.subject}</Typography> + <Typography variant="subtitle2">{getTimePeriod(event)}</Typography> + </Box> + {event.webLink && ( + <Tooltip title="Open in Calendar"> + <Link + data-testid="open-calendar-link" + to={event.webLink} + onClick={_e => {}} + noTrack + > + <IconButton> + <ArrowForwardIcon /> + </IconButton> + </Link> + </Tooltip> + )} + </Box> + {onlineMeetingLink && ( + <Link to={onlineMeetingLink} onClick={_e => {}} noTrack> + Join Online Meeting + </Link> + )} + + {event.bodyPreview && ( + <> + <Divider className={classes.divider} variant="fullWidth" /> + <Box + className={classes.description} + dangerouslySetInnerHTML={{ + __html: DOMPurify.sanitize( + (event.body && event.body.content) || '', + { + USE_PROFILES: { html: true }, + }, + ), + }} + /> + </> + )} + + {event.attendees && ( + <> + <Divider className={classes.divider} variant="fullWidth" /> + <Box> + <Typography variant="subtitle2">Attendees</Typography> + <Box mb={1} /> + {sortBy(event.attendees || [], 'emailAddress').map(user => ( + <AttendeeChip + key={user.emailAddress?.address || ''} + user={user} + /> + ))} + </Box> + </> + )} + </Box> + ); +}; diff --git a/plugins/microsoft-calendar/src/components/CalendarSelect.tsx b/plugins/microsoft-calendar/src/components/CalendarSelect.tsx new file mode 100644 index 0000000000..59e2376079 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/CalendarSelect.tsx @@ -0,0 +1,92 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { sortBy } from 'lodash'; +import React from 'react'; + +import { + Checkbox, + FormControl, + Input, + ListItemText, + MenuItem, + Select, + Typography, + makeStyles, +} from '@material-ui/core'; +import { MicrosoftCalendar } from '../api'; + +const useStyles = makeStyles( + { + formControl: { + width: 120, + }, + selectedCalendars: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + }, + { + name: 'MicrosoftCalendarSelect', + }, +); + +type CalendarSelectProps = { + disabled: boolean; + selectedCalendarId?: string; + setSelectedCalendarId: (value: string) => void; + calendars: MicrosoftCalendar[]; +}; + +export const CalendarSelect = ({ + disabled, + selectedCalendarId, + setSelectedCalendarId, + calendars, +}: CalendarSelectProps) => { + const classes = useStyles(); + return ( + <FormControl className={classes.formControl}> + <Select + labelId="calendars-label" + disabled={disabled || calendars.length === 0} + value={selectedCalendarId || ''} + onChange={async e => setSelectedCalendarId(e.target.value as string)} + input={<Input />} + renderValue={selected => { + return ( + <Typography className={classes.selectedCalendars} variant="body2"> + {calendars.find(c => c.id === selected)?.name} + </Typography> + ); + }} + MenuProps={{ + PaperProps: { + style: { + width: 350, + }, + }, + }} + > + {sortBy(calendars, 'name').map(c => ( + <MenuItem key={c.id} value={c.id}> + <Checkbox checked={c.id === selectedCalendarId} /> + <ListItemText primary={c.name} /> + </MenuItem> + ))} + </Select> + </FormControl> + ); +}; diff --git a/plugins/microsoft-calendar/src/components/MicrosoftCalendar.tsx b/plugins/microsoft-calendar/src/components/MicrosoftCalendar.tsx new file mode 100644 index 0000000000..3bb15f7762 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/MicrosoftCalendar.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +import { CalendarCard } from './CalendarCard'; + +const queryClient = new QueryClient(); + +export const MicrosoftCalendar = () => { + return ( + <QueryClientProvider client={queryClient}> + <CalendarCard /> + </QueryClientProvider> + ); +}; diff --git a/plugins/microsoft-calendar/src/components/SignInContent.tsx b/plugins/microsoft-calendar/src/components/SignInContent.tsx new file mode 100644 index 0000000000..d33698f68c --- /dev/null +++ b/plugins/microsoft-calendar/src/components/SignInContent.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Box, Button, styled } from '@material-ui/core'; +import { CalendarEvent } from './CalendarEvent'; +import mockEvents from './eventMock.json'; +import { MicrosoftCalendarEvent } from '../api'; + +type Props = { + handleAuthClick: React.MouseEventHandler<HTMLElement>; +}; + +const TransparentBox = styled(Box)({ + opacity: 0.3, + filter: 'blur(1.5px)', +}); + +export const SignInContent = ({ handleAuthClick }: Props) => { + return ( + <Box position="relative" height="100%" width="100%"> + <TransparentBox p={1}> + {(mockEvents as MicrosoftCalendarEvent[]).map(event => ( + <CalendarEvent key={event.id} event={event} /> + ))} + </TransparentBox> + + <Box + height="100%" + width="100%" + display="flex" + justifyContent="center" + alignItems="center" + position="absolute" + left={0} + top={0} + > + <Button + variant="contained" + color="primary" + onClick={handleAuthClick} + size="large" + > + Sign in + </Button> + </Box> + </Box> + ); +}; diff --git a/plugins/microsoft-calendar/src/components/eventMock.json b/plugins/microsoft-calendar/src/components/eventMock.json new file mode 100644 index 0000000000..b648d1b486 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/eventMock.json @@ -0,0 +1,138 @@ +[ + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", + "createdDateTime": "2022-12-22T08:28:36.6131501Z", + "lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z", + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", + "categories": [], + "transactionId": null, + "originalStartTimeZone": "India Standard Time", + "originalEndTimeZone": "India Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "Abhay Soni", + "bodyPreview": "", + "importance": "normal", + "sensitivity": "normal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": false, + "responseRequested": true, + "seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", + "showAs": "tentative", + "type": "occurrence", + "webLink": "", + "onlineMeetingUrl": "", + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "notResponded", + "time": "0001-01-01T00:00:00Z" + }, + "body": { + "contentType": "html", + "content": "" + }, + "start": { + "dateTime": "2023-01-16T11:00:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "end": { + "dateTime": "2023-01-16T11:30:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "attendees": [], + "organizer": { + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://abhay-soni-developer.github.io/MyReusme/" + } + }, + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", + "createdDateTime": "2022-12-22T08:28:36.6131501Z", + "lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z", + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", + "categories": [], + "transactionId": null, + "originalStartTimeZone": "India Standard Time", + "originalEndTimeZone": "India Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "https://abhay-soni-developer.github.io/MyReusme/", + "bodyPreview": "", + "importance": "normal", + "sensitivity": "normal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": false, + "responseRequested": true, + "seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", + "showAs": "tentative", + "type": "occurrence", + "webLink": "", + "onlineMeetingUrl": "", + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "notResponded", + "time": "0001-01-01T00:00:00Z" + }, + "body": { + "contentType": "html", + "content": "" + }, + "start": { + "dateTime": "2023-01-16T11:00:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "end": { + "dateTime": "2023-01-16T11:30:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "attendees": [], + "organizer": { + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://abhay-soni-developer.github.io/MyReusme/" + } + } +] diff --git a/plugins/microsoft-calendar/src/components/index.ts b/plugins/microsoft-calendar/src/components/index.ts new file mode 100644 index 0000000000..8149695804 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { MicrosoftCalendar } from './MicrosoftCalendar'; diff --git a/plugins/microsoft-calendar/src/components/util.ts b/plugins/microsoft-calendar/src/components/util.ts new file mode 100644 index 0000000000..dd52402d9d --- /dev/null +++ b/plugins/microsoft-calendar/src/components/util.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DateTime } from 'luxon'; + +import { MicrosoftCalendarEvent } from '../api'; + +export function getOnlineMeetingLink(event: MicrosoftCalendarEvent) { + const onlineEntrypoint = + event.onlineMeeting?.joinUrl || event.onlineMeetingUrl; + if (onlineEntrypoint) { + return onlineEntrypoint; + } + return ''; +} + +export function getTimePeriod(event: MicrosoftCalendarEvent) { + if (isAllDay(event)) { + return getAllDayTimePeriod(event); + } + + const format: Intl.DateTimeFormatOptions = { + hour: '2-digit', + minute: '2-digit', + }; + + const startTime = DateTime.fromISO(event.start?.dateTime || ''); + const endTime = DateTime.fromISO(event.end?.dateTime || ''); + + return `${startTime.toLocaleString(format)} - ${endTime.toLocaleString( + format, + )}`; +} + +function getAllDayTimePeriod(event: MicrosoftCalendarEvent) { + const format: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric' }; + const startTime = DateTime.fromISO(event.start?.dateTime || ''); + const endTime = DateTime.fromISO(event.end?.dateTime || '').minus({ day: 1 }); + + if (startTime.toISO() === endTime.toISO()) { + return startTime.toLocaleString(format); + } + + return `${startTime.toLocaleString(format)} - ${endTime.toLocaleString( + format, + )}`; +} + +export function isPassed(event: MicrosoftCalendarEvent) { + if (!event.end?.dateTime) return false; + const eventDate = DateTime.fromISO(event.end?.dateTime!); + return DateTime.now() >= eventDate; +} + +export function isAllDay(event: MicrosoftCalendarEvent) { + const startTime = DateTime.fromISO(event.start?.dateTime || ''); + const endTime = DateTime.fromISO(event.end?.dateTime || ''); + + return endTime.diff(startTime, 'day').days >= 1; +} + +export function getStartDate(event: MicrosoftCalendarEvent) { + return DateTime.fromISO(event.start?.dateTime || ''); +} diff --git a/plugins/microsoft-calendar/src/hooks/index.ts b/plugins/microsoft-calendar/src/hooks/index.ts new file mode 100644 index 0000000000..a10f6a6c3a --- /dev/null +++ b/plugins/microsoft-calendar/src/hooks/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 { useCalendarsQuery } from './useCalendarsQuery'; +export { useEventsQuery } from './useEventsQuery'; +export { useSignIn } from './useSignIn'; diff --git a/plugins/microsoft-calendar/src/hooks/useCalendarsQuery.ts b/plugins/microsoft-calendar/src/hooks/useCalendarsQuery.ts new file mode 100644 index 0000000000..47a7990b58 --- /dev/null +++ b/plugins/microsoft-calendar/src/hooks/useCalendarsQuery.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 { useQuery } from '@tanstack/react-query'; + +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; + +import { microsoftCalendarApiRef } from '../api'; + +type Options = { + enabled: boolean; + refreshTime?: number; +}; + +export const useCalendarsQuery = ({ enabled }: Options) => { + const calendarApi = useApi(microsoftCalendarApiRef); + const errorApi = useApi(errorApiRef); + + return useQuery( + ['calendars-list'], + async () => { + const calendars = []; + const value = await calendarApi.getCalendars(); + calendars.push(...value); + return calendars; + }, + { + enabled, + // old data will be returned if last request was made within 3 mins. + staleTime: 180000, + refetchInterval: 3600000, + onError: () => { + errorApi.post({ + name: 'API error', + message: 'Failed to fetch calendars.', + }); + }, + }, + ); +}; diff --git a/plugins/microsoft-calendar/src/hooks/useEventsQuery.ts b/plugins/microsoft-calendar/src/hooks/useEventsQuery.ts new file mode 100644 index 0000000000..5e9dc76812 --- /dev/null +++ b/plugins/microsoft-calendar/src/hooks/useEventsQuery.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useQuery } from '@tanstack/react-query'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { microsoftCalendarApiRef } from '../api'; + +type Options = { + calendarId: string; + timeMin: string; + timeMax: string; + enabled?: boolean; + timeZone?: string; + refreshTime?: number; +}; + +export const useEventsQuery = ({ + calendarId, + enabled, + timeMin, + timeMax, + timeZone, +}: Options) => { + const calendarApi = useApi(microsoftCalendarApiRef); + return useQuery( + ['calendarEvents', calendarId, timeMin, timeMax, timeZone], + async () => { + const data = await calendarApi.getEvents( + calendarId, + { + startDateTime: timeMin, + endDateTime: timeMax, + }, + { + Prefer: `outlook.timezone="${timeZone}"`, + }, + ); + return data; + }, + { + cacheTime: 300000, + enabled, + refetchInterval: 60000, + refetchIntervalInBackground: true, + }, + ); +}; diff --git a/plugins/microsoft-calendar/src/hooks/useSignIn.ts b/plugins/microsoft-calendar/src/hooks/useSignIn.ts new file mode 100644 index 0000000000..8b3ac54f92 --- /dev/null +++ b/plugins/microsoft-calendar/src/hooks/useSignIn.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useCallback, useState } from 'react'; + +import { microsoftAuthApiRef, useApi } from '@backstage/core-plugin-api'; + +export const useSignIn = () => { + const [isSignedIn, setSignedIn] = useState(false); + const [isInitialized, setInitialized] = useState(false); + const authApi = useApi(microsoftAuthApiRef); + + const signIn = useCallback( + async (optional = false) => { + const token = await authApi.getAccessToken('Calendars.Read', { + optional, + instantPopup: !optional, + }); + + setSignedIn(!!token); + setInitialized(true); + }, + [authApi, setSignedIn], + ); + + return { isSignedIn, isInitialized, signIn }; +}; diff --git a/plugins/microsoft-calendar/src/icons/calendar.svg b/plugins/microsoft-calendar/src/icons/calendar.svg new file mode 100644 index 0000000000..79895da87a --- /dev/null +++ b/plugins/microsoft-calendar/src/icons/calendar.svg @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 512 512" xml:space="preserve"> +<circle style="fill:#45B39C;" cx="256" cy="256" r="256"/> +<path style="opacity:0.1;enable-background:new ;" d="M512,256c0-10.816-0.747-21.445-2.048-31.904L399.781,113.925l-0.107-0.107 + c-2.24-2.773-5.707-4.48-9.547-4.48H121.861c-6.933,0-12.533,5.6-12.533,12.533v268.267c0,3.253,1.227,6.187,3.253,8.373 + l111.424,111.424C234.496,511.248,245.157,512,256,512C397.387,512,512,397.387,512,256z"/> +<path style="fill:#E56353;" d="M390.133,109.333H121.867c-6.923,0-12.533,5.611-12.533,12.533v42.277h293.333v-42.277 + C402.667,114.944,397.056,109.333,390.133,109.333z"/> +<path style="fill:#D5D6DB;" d="M109.333,164.144v225.989c0,6.923,5.611,12.533,12.533,12.533h268.267 + c6.923,0,12.533-5.611,12.533-12.533V164.144H109.333z"/> +<path style="fill:#EBF0F3;" d="M109.333,164.144v215.323c0,6.923,5.611,12.533,12.533,12.533h268.267 + c6.923,0,12.533-5.611,12.533-12.533V164.144H109.333z"/> +<circle style="fill:#D15241;" cx="144.8" cy="150.827" r="7.675"/> +<circle style="fill:#D5D6DB;" cx="144.8" cy="177.493" r="7.675"/> +<path style="fill:#64798A;" d="M144.789,145.771c-2.917,0-5.285,2.368-5.285,5.285v26.176c0,2.917,2.368,5.285,5.285,5.285 + c2.917,0,5.285-2.368,5.285-5.285v-26.176C150.069,148.139,147.707,145.771,144.789,145.771z"/> +<circle style="fill:#D15241;" cx="367.216" cy="150.827" r="7.675"/> +<circle style="fill:#D5D6DB;" cx="367.216" cy="177.493" r="7.675"/> +<path style="fill:#64798A;" d="M367.216,145.771c-2.917,0-5.285,2.368-5.285,5.285v26.176c0,2.917,2.363,5.285,5.285,5.285 + c2.917,0,5.285-2.368,5.285-5.285v-26.176C372.496,148.139,370.133,145.771,367.216,145.771z"/> +<circle style="fill:#D15241;" cx="293.072" cy="150.827" r="7.675"/> +<circle style="fill:#D5D6DB;" cx="293.072" cy="177.493" r="7.675"/> +<path style="fill:#64798A;" d="M293.072,145.771c-2.917,0-5.285,2.368-5.285,5.285v26.176c0,2.917,2.363,5.285,5.285,5.285 + c2.917,0,5.285-2.368,5.285-5.285v-26.176C298.357,148.139,295.989,145.771,293.072,145.771z"/> +<circle style="fill:#D15241;" cx="218.933" cy="150.827" r="7.675"/> +<circle style="fill:#D5D6DB;" cx="218.933" cy="177.493" r="7.675"/> +<path style="fill:#64798A;" d="M218.928,145.771c-2.917,0-5.285,2.368-5.285,5.285v26.176c0,2.917,2.363,5.285,5.285,5.285 + c2.917,0,5.285-2.368,5.285-5.285v-26.176C224.213,148.139,221.845,145.771,218.928,145.771z"/> +<rect x="218.491" y="210.437" style="fill:#D5D6DB;" width="28.384" height="28.384"/> +<rect x="265.125" y="210.437" style="fill:#44C4A1;" width="28.384" height="28.384"/> +<rect x="311.733" y="210.437" style="fill:#D5D6DB;" width="28.384" height="28.384"/> +<rect x="358.4" y="210.437" style="fill:#E56353;" width="28.384" height="28.384"/> +<g> + <rect x="125.221" y="255.019" style="fill:#D5D6DB;" width="28.384" height="28.384"/> + <rect x="171.84" y="255.019" style="fill:#D5D6DB;" width="28.384" height="28.384"/> + <rect x="218.491" y="255.019" style="fill:#D5D6DB;" width="28.384" height="28.384"/> +</g> +<rect x="265.125" y="255.019" style="fill:#44C4A1;" width="28.384" height="28.384"/> +<rect x="311.733" y="255.019" style="fill:#D5D6DB;" width="28.384" height="28.384"/> +<rect x="358.4" y="255.019" style="fill:#E56353;" width="28.384" height="28.384"/> +<g> + <rect x="125.221" y="299.573" style="fill:#D5D6DB;" width="28.384" height="28.384"/> + <rect x="171.84" y="299.573" style="fill:#D5D6DB;" width="28.384" height="28.384"/> + <rect x="218.491" y="299.573" style="fill:#D5D6DB;" width="28.384" height="28.384"/> +</g> +<rect x="265.125" y="299.573" style="fill:#44C4A1;" width="28.384" height="28.384"/> +<rect x="311.733" y="299.573" style="fill:#D5D6DB;" width="28.384" height="28.384"/> +<rect x="358.4" y="299.573" style="fill:#E56353;" width="28.384" height="28.384"/> +<g> + <rect x="125.221" y="344.16" style="fill:#D5D6DB;" width="28.384" height="28.384"/> + <rect x="171.84" y="344.16" style="fill:#D5D6DB;" width="28.384" height="28.384"/> + <rect x="218.491" y="344.16" style="fill:#D5D6DB;" width="28.384" height="28.384"/> +</g> +</svg> \ No newline at end of file diff --git a/plugins/microsoft-calendar/src/icons/webcam.svg b/plugins/microsoft-calendar/src/icons/webcam.svg new file mode 100644 index 0000000000..356bb69215 --- /dev/null +++ b/plugins/microsoft-calendar/src/icons/webcam.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M778.666667 938.666667h-533.333334c-23.466667 0-38.4-25.6-27.733333-46.933334L277.333333 789.333333h469.333334l57.6 102.4c12.8 21.333333-2.133333 46.933333-25.6 46.933334z" fill="#455A64" /><path d="M512 490.666667m-384 0a384 384 0 1 0 768 0 384 384 0 1 0-768 0Z" fill="#78909C" /><path d="M512 746.666667c-140.8 0-256-115.2-256-256s115.2-256 256-256 256 115.2 256 256-115.2 256-256 256z" fill="#455A64" /><path d="M512 490.666667m-192 0a192 192 0 1 0 384 0 192 192 0 1 0-384 0Z" fill="#42A5F5" /><path d="M614.4 426.666667c-25.6-29.866667-64-46.933333-102.4-46.933334s-76.8 17.066667-102.4 46.933334c-10.666667 10.666667-8.533333 27.733333 2.133333 38.4s27.733333 8.533333 38.4-2.133334c32-36.266667 91.733333-36.266667 123.733334 0 6.4 6.4 12.8 8.533333 21.333333 8.533334 6.4 0 12.8-2.133333 19.2-6.4 8.533333-8.533333 10.666667-27.733333 0-38.4z" fill="#90CAF9" /></svg> \ No newline at end of file diff --git a/plugins/microsoft-calendar/src/index.ts b/plugins/microsoft-calendar/src/index.ts new file mode 100644 index 0000000000..6867ca31ac --- /dev/null +++ b/plugins/microsoft-calendar/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { MicrosoftCalendarCard } from './plugin'; +export { MicrosoftCalendarApiClient, microsoftCalendarApiRef } from './api'; diff --git a/plugins/microsoft-calendar/src/plugin.test.ts b/plugins/microsoft-calendar/src/plugin.test.ts new file mode 100644 index 0000000000..7da86e176b --- /dev/null +++ b/plugins/microsoft-calendar/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { microsoftCalendarPlugin } from './plugin'; + +describe('microsoft-calendar', () => { + it('should export plugin', () => { + expect(microsoftCalendarPlugin).toBeDefined(); + }); +}); diff --git a/plugins/microsoft-calendar/src/plugin.ts b/plugins/microsoft-calendar/src/plugin.ts new file mode 100644 index 0000000000..644d368d97 --- /dev/null +++ b/plugins/microsoft-calendar/src/plugin.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createPlugin, + microsoftAuthApiRef, + fetchApiRef, + createApiFactory, + createComponentExtension, +} from '@backstage/core-plugin-api'; +import { + microsoftCalendarApiRef, + MicrosoftCalendarApiClient, +} from './api/index'; + +export const microsoftCalendarPlugin = createPlugin({ + id: 'microsoft-calendar', + apis: [ + createApiFactory({ + api: microsoftCalendarApiRef, + deps: { authApi: microsoftAuthApiRef, fetchApi: fetchApiRef }, + factory(deps) { + return new MicrosoftCalendarApiClient(deps); + }, + }), + ], +}); + +/** @public */ +export const MicrosoftCalendarCard = microsoftCalendarPlugin.provide( + createComponentExtension({ + name: 'MicrosoftCalendarCard', + component: { + lazy: () => import('./components').then(m => m.MicrosoftCalendar), + }, + }), +); diff --git a/plugins/microsoft-calendar/src/setupTests.ts b/plugins/microsoft-calendar/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/microsoft-calendar/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index d01c1cd3c2..c3de323d2d 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/plugin-newrelic-dashboard +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## 0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + +## 0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.2.7 ### Patch Changes diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index a6ece89000..462352b69e 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.2.7", + "version": "0.2.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 8332f157af..ba75aface5 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-newrelic +## 0.3.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.3.33 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## 0.3.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.3.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.3.32 ### Patch Changes diff --git a/plugins/newrelic/README.md b/plugins/newrelic/README.md index e7cdd0df4a..8ac7ed2c72 100644 --- a/plugins/newrelic/README.md +++ b/plugins/newrelic/README.md @@ -8,36 +8,77 @@ Website: [https://newrelic.com](https://newrelic.com) ## Getting Started This plugin uses the Backstage proxy to securely communicate with New Relic's -APIs. Add the following to your `app-config.yaml` to enable this configuration: +APIs. -```yaml -proxy: - '/newrelic/apm/api': - target: https://api.newrelic.com/v2 - headers: - X-Api-Key: ${NEW_RELIC_REST_API_KEY} -``` +1. Add the following to your `app-config.yaml` to enable this configuration: -In your production deployment of Backstage, you would also need to ensure that -you've set the `NEW_RELIC_REST_API_KEY` environment variable before starting -the backend. + ```yaml + proxy: + '/newrelic/apm/api': + target: https://api.newrelic.com/v2 + headers: + X-Api-Key: ${NEW_RELIC_REST_API_KEY} + ``` -While working locally, you may wish to hard-code your API key in your -`app-config.local.yaml` like this: + There is some types of api key on new relic, to this use must be `User` type of key, In your production deployment of Backstage, you would also need to ensure that + you've set the `NEW_RELIC_REST_API_KEY` environment variable before starting + the backend. -```yaml -# app-config.local.yaml -proxy: - '/newrelic/apm/api': - headers: - X-Api-Key: NRRA-YourActualApiKey -``` + While working locally, you may wish to hard-code your API key in your + `app-config.local.yaml` like this: -Read more about how to find or generate this key in -[New Relic's Documentation](https://docs.newrelic.com/docs/apis/get-started/intro-apis/types-new-relic-api-keys#rest-api-key). + ```yaml + # app-config.local.yaml + proxy: + '/newrelic/apm/api': + headers: + X-Api-Key: NRRA-YourActualApiKey + ``` -See if it's working by visiting the New Relic Plugin Path: -[/newrelic](http://localhost:3000/newrelic) + Read more about how to find or generate this key in + [New Relic's Documentation](https://docs.newrelic.com/docs/apis/get-started/intro-apis/types-new-relic-api-keys#rest-api-key). + + See if it's working by visiting the New Relic Plugin Path: + [/newrelic](http://localhost:3000/newrelic) + +2. Add a dependency to your `packages/app/package.json`: + ```sh + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-newrelic + ``` +3. Add the `NewRelicPage` to your `packages/app/src/App.tsx`: + + ```tsx + <FlatRoutes> + … + <Route path="/newrelic" element={<NewRelicPage />} /> + </FlatRoutes> + ``` + +4. Add link to New Relic to your sidebar + + ```typescript + // packages/app/src/components/Root/Root.tsx + import ExtensionIcon from '@material-ui/icons/ExtensionOutlined'; + + ... + + export const Root = ({ children }: PropsWithChildren<{}>) => ( + <SidebarPage> + <Sidebar> + ... + <SidebarItem icon={ExtensionIcon} to="newrelic" text="New Relic" /> + ... + </Sidebar> + </SidebarPage> + ); + + ``` + +5. Navigate to your.domain.com/newrelic. + + At this step you must be able to see a page like that + <img src="./src/assets/img/newrelic-plugin-apm.png" alt="New Relic Plugin APM" /> ## Features diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 022eda45bd..9d183f34d6 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.32", + "version": "0.3.34-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -54,7 +54,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/octopus-deploy/.eslintrc.js b/plugins/octopus-deploy/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/octopus-deploy/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/octopus-deploy/CHANGELOG.md b/plugins/octopus-deploy/CHANGELOG.md new file mode 100644 index 0000000000..15be227555 --- /dev/null +++ b/plugins/octopus-deploy/CHANGELOG.md @@ -0,0 +1,16 @@ +# @backstage/plugin-octopus-deploy + +## 0.1.0-next.0 + +### Minor Changes + +- b7fff1171f: Initial version + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 diff --git a/plugins/octopus-deploy/README.md b/plugins/octopus-deploy/README.md new file mode 100644 index 0000000000..abc0559896 --- /dev/null +++ b/plugins/octopus-deploy/README.md @@ -0,0 +1,61 @@ +# Octopus Deploy Plugin + +Welcome to the octopus-deploy plugin! + +## Features + +- Display the deployment status of the most recent releases for a project in Octopus Deploy straight from the Backstage catalog + +## Getting started + +This plugin (currently) uses the Backstage proxy to securely communicate with the Octopus Deploy API. + +To use it, you will need to generate an [API Key](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) within Octopus Deploy. + +1. Add the following to your app-config.yaml to enable the proxy: + +``` +// app-config.yaml +proxy: + '/octopus-deploy': + target: '<your-octopus-server-url>' + headers: + X-Octopus-ApiKey: ${OCTOPUS_API_KEY} +``` + +2. Add the following to `EntityPage.tsx` to display Octopus Releases + +``` +// In packages/app/src/components/catalog/EntityPage.tsx +import { + isOctopusDeployAvailable + EntityOctopusDeployContent +} from '@backstage/plugin-octopus-deploy'; + +const cicdContent = ( + <EntitySwitch> + {/* other components... */} + <EntitySwitch.Case if={isOctopusDeployAvailable}> + <EntityOctopusDeployContent defaultLimit={25} /> + </EntitySwitch.Case> + </EntitySwitch> +) +``` + +3. Add `octopus.com/project-id` annotation in catalog descriptor file + +To obtain a projects ID you will have to query the Octopus API. In the future we'll add support for using a projects slug as well. + +``` +// catalog-info.yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + # ... + annotations: + octopus.com/project-id: Projects-102 +spec: + type: service +``` + +All set , you will be able to see the plugin in action! diff --git a/plugins/octopus-deploy/api-report.md b/plugins/octopus-deploy/api-report.md new file mode 100644 index 0000000000..82512ebcef --- /dev/null +++ b/plugins/octopus-deploy/api-report.md @@ -0,0 +1,86 @@ +## API Report File for "@backstage/plugin-octopus-deploy" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="react" /> + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { FetchApi } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const EntityOctopusDeployContent: (props: { + defaultLimit?: number | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const isOctopusDeployAvailable: (entity: Entity) => boolean; + +// @public (undocumented) +export const OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION = 'octopus.com/project-id'; + +// @public (undocumented) +export interface OctopusDeployApi { + // (undocumented) + getReleaseProgression( + projectId: string, + releaseHistoryCount: number, + ): Promise<OctopusProgression>; +} + +// @public (undocumented) +export const octopusDeployApiRef: ApiRef<OctopusDeployApi>; + +// @public (undocumented) +export class OctopusDeployClient implements OctopusDeployApi { + constructor(options: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + proxyPathBase?: string; + }); + // (undocumented) + getReleaseProgression( + projectId: string, + releaseHistoryCount: number, + ): Promise<OctopusProgression>; +} + +// @public (undocumented) +export type OctopusDeployment = { + State: string; +}; + +// @public (undocumented) +export const octopusDeployPlugin: BackstagePlugin<{}, {}, {}>; + +// @public (undocumented) +export type OctopusEnvironment = { + Id: string; + Name: string; +}; + +// @public (undocumented) +export type OctopusProgression = { + Environments: OctopusEnvironment[]; + Releases: OctopusReleaseProgression[]; +}; + +// @public (undocumented) +export type OctopusRelease = { + Id: string; + Version: string; +}; + +// @public (undocumented) +export type OctopusReleaseProgression = { + Release: OctopusRelease; + Deployments: { + [key: string]: OctopusDeployment[]; + }; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/octopus-deploy/dev/index.tsx b/plugins/octopus-deploy/dev/index.tsx new file mode 100644 index 0000000000..d3d6cb3215 --- /dev/null +++ b/plugins/octopus-deploy/dev/index.tsx @@ -0,0 +1,28 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { octopusDeployPlugin } from '../src/plugin'; +import { EntityPageOctopusDeploy } from '../src/components/EntityPageOctopusDeploy'; + +createDevApp() + .registerPlugin(octopusDeployPlugin) + .addPage({ + element: <EntityPageOctopusDeploy />, + title: 'Root Page', + path: '/octopus-deploy', + }) + .render(); diff --git a/plugins/octopus-deploy/package.json b/plugins/octopus-deploy/package.json new file mode 100644 index 0000000000..c469b55c38 --- /dev/null +++ b/plugins/octopus-deploy/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-octopus-deploy", + "version": "0.1.0-next.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/octopus-deploy/src/api/index.ts b/plugins/octopus-deploy/src/api/index.ts new file mode 100644 index 0000000000..9cba49d6c0 --- /dev/null +++ b/plugins/octopus-deploy/src/api/index.ts @@ -0,0 +1,118 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createApiRef, + DiscoveryApi, + FetchApi, +} from '@backstage/core-plugin-api'; + +/** @public */ +export type OctopusProgression = { + Environments: OctopusEnvironment[]; + Releases: OctopusReleaseProgression[]; +}; + +/** @public */ +export type OctopusEnvironment = { + Id: string; + Name: string; +}; + +/** @public */ +export type OctopusReleaseProgression = { + Release: OctopusRelease; + Deployments: { [key: string]: OctopusDeployment[] }; +}; + +/** @public */ +export type OctopusRelease = { + Id: string; + Version: string; +}; + +/** @public */ +export type OctopusDeployment = { + State: string; +}; + +/** @public */ +export const octopusDeployApiRef = createApiRef<OctopusDeployApi>({ + id: 'plugin.octopusdeploy.service', +}); + +const DEFAULT_PROXY_PATH_BASE = '/octopus-deploy'; + +/** @public */ +export interface OctopusDeployApi { + getReleaseProgression( + projectId: string, + releaseHistoryCount: number, + ): Promise<OctopusProgression>; +} + +/** @public */ +export class OctopusDeployClient implements OctopusDeployApi { + private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; + private readonly proxyPathBase: string; + + constructor(options: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + proxyPathBase?: string; + }) { + this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi; + this.proxyPathBase = options.proxyPathBase ?? DEFAULT_PROXY_PATH_BASE; + } + + async getReleaseProgression( + projectId: string, + releaseHistoryCount: number, + ): Promise<OctopusProgression> { + const url = await this.getApiUrl(projectId, releaseHistoryCount); + + const response = await this.fetchApi.fetch(url); + + let responseJson; + + try { + responseJson = await response.json(); + } catch (e) { + responseJson = { releases: [] }; + } + + if (response.status !== 200) { + throw new Error( + `Error communicating with Octopus Deploy: ${ + responseJson?.error?.title || response.statusText + }`, + ); + } + + return responseJson; + } + + private async getApiUrl(projectId: string, releaseHistoryCount: number) { + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + const queryParameters = new URLSearchParams({ + releaseHistoryCount: releaseHistoryCount.toString(), + }); + return `${proxyUrl}${this.proxyPathBase}/projects/${encodeURIComponent( + projectId, + )}/progression?${queryParameters}`; + } +} diff --git a/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/EntityPageOctopusDeploy.tsx b/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/EntityPageOctopusDeploy.tsx new file mode 100644 index 0000000000..de28edb9e0 --- /dev/null +++ b/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/EntityPageOctopusDeploy.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useEntity } from '@backstage/plugin-catalog-react'; +import { useReleases } from '../../hooks/useReleases'; +import { getAnnotationFromEntity } from '../../utils/getAnnotationFromEntity'; +import React from 'react'; +import { ReleaseTable } from '../ReleaseTable'; + +export const EntityPageOctopusDeploy = (props: { defaultLimit?: number }) => { + const { entity } = useEntity(); + + const projectId = getAnnotationFromEntity(entity); + + const { environments, releases, loading, error } = useReleases( + projectId, + props.defaultLimit ?? 3, + ); + + return ( + <ReleaseTable + environments={environments} + releases={releases} + loading={loading} + error={error} + /> + ); +}; diff --git a/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/index.ts b/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/index.ts new file mode 100644 index 0000000000..e168735130 --- /dev/null +++ b/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityPageOctopusDeploy } from './EntityPageOctopusDeploy'; diff --git a/plugins/octopus-deploy/src/components/OctopusDeployIcon/OctopusDeployIcon.tsx b/plugins/octopus-deploy/src/components/OctopusDeployIcon/OctopusDeployIcon.tsx new file mode 100644 index 0000000000..626e61ccd8 --- /dev/null +++ b/plugins/octopus-deploy/src/components/OctopusDeployIcon/OctopusDeployIcon.tsx @@ -0,0 +1,31 @@ +/* + * 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 { SvgIcon, SvgIconProps } from '@material-ui/core'; + +import React from 'react'; + +export const OctopusDeployIcon = (props: SvgIconProps) => ( + <SvgIcon {...props} viewBox="0 0 140 140"> + <defs> + <style>.cls-1{fill:#fff;}</style> + </defs> + <path + className="cls-1" + d="M19.13 103.13c9.35-6 20.33-16.34 16.16-28.5C33 68 29.87 62.33 29.45 55.17a43.84 43.84 0 0 1 2.66-17.83C40.49 14.85 65.4 4.2 87.92 11c20.85 6.26 35.2 30.48 26.56 51.58-5 12.18-7.22 21.58 3.9 31.05 3 2.57 10.33 6.42 10.31 11 0 6-11.74-1.27-13-2.31 1.49 2.6 16.24 18 6.85 19.08-8.64 1-16.27-11.06-21.47-16.25-8.72-8.7-7.21 10.55-7.25 14.53-.06 6.28-4.5 19-12.46 10.72-6.58-6.84-4.09-17.76-8.66-25.36-5-8.33-13.34 8.33-15.42 11.37-2.33 3.4-14 19.89-18.59 11.1-3.76-7.13 2.25-18.3 5.2-24.75-1.08 2.34-8.71 5.8-10.94 6.93-5 2.54-10.15 4.05-15.82 3.65-11.73-.84-2.73-7.07 2-10.15z" + /> + </SvgIcon> +); diff --git a/plugins/octopus-deploy/src/components/OctopusDeployIcon/index.ts b/plugins/octopus-deploy/src/components/OctopusDeployIcon/index.ts new file mode 100644 index 0000000000..d78e1bfc7f --- /dev/null +++ b/plugins/octopus-deploy/src/components/OctopusDeployIcon/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { OctopusDeployIcon } from './OctopusDeployIcon'; diff --git a/plugins/octopus-deploy/src/components/ReleaseTable/ReleaseTable.tsx b/plugins/octopus-deploy/src/components/ReleaseTable/ReleaseTable.tsx new file mode 100644 index 0000000000..bc185546b1 --- /dev/null +++ b/plugins/octopus-deploy/src/components/ReleaseTable/ReleaseTable.tsx @@ -0,0 +1,153 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Box, Typography } from '@material-ui/core'; +import { OctopusEnvironment, OctopusReleaseProgression } from '../../api'; + +import React from 'react'; +import { + ResponseErrorPanel, + StatusAborted, + StatusError, + StatusOK, + StatusPending, + StatusRunning, + StatusWarning, + Table, + TableColumn, +} from '@backstage/core-components'; +import { OctopusDeployIcon } from '../OctopusDeployIcon'; + +type ReleaseTableProps = { + environments?: OctopusEnvironment[]; + releases?: OctopusReleaseProgression[]; + loading: boolean; + error?: Error; +}; + +export const getDeploymentStatusComponent = (state: string | undefined) => { + switch (state) { + case 'Success': + return ( + <Typography component="span"> + <StatusOK /> Success + </Typography> + ); + case 'Queued': + return ( + <Typography component="span"> + <StatusPending /> Queued + </Typography> + ); + case 'Executing': + return ( + <Typography component="span"> + <StatusRunning /> Executing + </Typography> + ); + case 'Failed': + return ( + <Typography component="span"> + <StatusError /> Failed + </Typography> + ); + case 'Cancelling': + return ( + <Typography component="span"> + <StatusPending /> Cancelling + </Typography> + ); + case 'Canceled': + return ( + <Typography component="span"> + <StatusAborted /> Canceled + </Typography> + ); + case 'TimedOut': + return ( + <Typography component="span"> + <StatusWarning /> Timed Out + </Typography> + ); + default: + return ( + <Typography component="span"> + <StatusWarning /> Unknown + </Typography> + ); + } +}; + +export const ReleaseTable = ({ + environments, + releases, + loading, + error, +}: ReleaseTableProps) => { + if (error) { + return <ResponseErrorPanel error={error} />; + } + + const columns: TableColumn[] = [ + { + title: 'Version', + field: 'Release.Version', + highlight: false, + width: 'auto', + }, + ...(environments?.map(env => ({ + title: env.Name, + width: 'auto', + render: (row: Partial<OctopusReleaseProgression>) => { + const deploymentsForEnvironment = row.Deployments + ? row.Deployments[env.Id] + : null; + if (deploymentsForEnvironment) { + return ( + <Box display="flex" alignItems="center"> + <Typography variant="button"> + {getDeploymentStatusComponent( + deploymentsForEnvironment[0].State, + )} + </Typography> + </Box> + ); + } + return <Box display="flex" alignItems="center" />; + }, + })) ?? []), + ]; + + return ( + <Table + isLoading={loading} + columns={columns} + options={{ + search: true, + paging: true, + pageSize: 5, + showEmptyDataSourceMessage: !loading, + }} + title={ + <Box display="flex" alignItems="center"> + <OctopusDeployIcon style={{ fontSize: 30 }} /> + <Box mr={1} /> + Octopus Deploy - Releases ({releases ? releases.length : 0}) + </Box> + } + data={releases ?? []} + /> + ); +}; diff --git a/plugins/octopus-deploy/src/components/ReleaseTable/index.ts b/plugins/octopus-deploy/src/components/ReleaseTable/index.ts new file mode 100644 index 0000000000..d0c679a48e --- /dev/null +++ b/plugins/octopus-deploy/src/components/ReleaseTable/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ReleaseTable } from './ReleaseTable'; diff --git a/plugins/octopus-deploy/src/constants.ts b/plugins/octopus-deploy/src/constants.ts new file mode 100644 index 0000000000..3bdc48252f --- /dev/null +++ b/plugins/octopus-deploy/src/constants.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @public */ +export const OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION = 'octopus.com/project-id'; diff --git a/plugins/octopus-deploy/src/hooks/useReleases.ts b/plugins/octopus-deploy/src/hooks/useReleases.ts new file mode 100644 index 0000000000..742ce78dca --- /dev/null +++ b/plugins/octopus-deploy/src/hooks/useReleases.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { + octopusDeployApiRef, + OctopusEnvironment, + OctopusReleaseProgression, +} from '../api'; + +export function useReleases( + projectId: string, + releaseHistoryCount: number, +): { + environments?: OctopusEnvironment[]; + releases?: OctopusReleaseProgression[]; + loading: boolean; + error?: Error; +} { + const api = useApi(octopusDeployApiRef); + + const { value, loading, error } = useAsync(() => { + return api.getReleaseProgression(projectId, releaseHistoryCount); + }, [api, projectId, releaseHistoryCount]); + + return { + environments: value?.Environments, + releases: value?.Releases, + loading, + error, + }; +} diff --git a/plugins/octopus-deploy/src/index.ts b/plugins/octopus-deploy/src/index.ts new file mode 100644 index 0000000000..e3264efaf0 --- /dev/null +++ b/plugins/octopus-deploy/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + octopusDeployPlugin, + EntityOctopusDeployContent, + isOctopusDeployAvailable, +} from './plugin'; + +export * from './api'; + +export { OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION } from './constants'; diff --git a/plugins/octopus-deploy/src/plugin.test.ts b/plugins/octopus-deploy/src/plugin.test.ts new file mode 100644 index 0000000000..a18e74f689 --- /dev/null +++ b/plugins/octopus-deploy/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { octopusDeployPlugin } from './plugin'; + +describe('octopus-deploy', () => { + it('should export plugin', () => { + expect(octopusDeployPlugin).toBeDefined(); + }); +}); diff --git a/plugins/octopus-deploy/src/plugin.ts b/plugins/octopus-deploy/src/plugin.ts new file mode 100644 index 0000000000..9365f93bdb --- /dev/null +++ b/plugins/octopus-deploy/src/plugin.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION } from './constants'; +import { octopusDeployEntityContentRouteRef } from './routes'; + +import { OctopusDeployClient, octopusDeployApiRef } from './api'; + +import { + createApiFactory, + createPlugin, + createRoutableExtension, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; + +import { Entity } from '@backstage/catalog-model'; + +/** @public */ +export const isOctopusDeployAvailable = (entity: Entity) => + Boolean(entity.metadata.annotations?.[OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION]); + +/** @public */ +export const octopusDeployPlugin = createPlugin({ + id: 'octopus-deploy', + apis: [ + createApiFactory({ + api: octopusDeployApiRef, + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new OctopusDeployClient({ discoveryApi, fetchApi }), + }), + ], +}); + +/** @public */ +export const EntityOctopusDeployContent = octopusDeployPlugin.provide( + createRoutableExtension({ + name: 'EntityOctopusDeployContent', + component: () => + import('./components/EntityPageOctopusDeploy').then( + m => m.EntityPageOctopusDeploy, + ), + mountPoint: octopusDeployEntityContentRouteRef, + }), +); diff --git a/plugins/octopus-deploy/src/routes.ts b/plugins/octopus-deploy/src/routes.ts new file mode 100644 index 0000000000..824b6ae113 --- /dev/null +++ b/plugins/octopus-deploy/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const octopusDeployEntityContentRouteRef = createRouteRef({ + id: 'octopus-deploy-entity-content', +}); diff --git a/plugins/octopus-deploy/src/setupTests.ts b/plugins/octopus-deploy/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/octopus-deploy/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/octopus-deploy/src/utils/getAnnotationFromEntity.ts b/plugins/octopus-deploy/src/utils/getAnnotationFromEntity.ts new file mode 100644 index 0000000000..e59a32235f --- /dev/null +++ b/plugins/octopus-deploy/src/utils/getAnnotationFromEntity.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION } from '../constants'; + +import { Entity } from '@backstage/catalog-model'; + +export function getAnnotationFromEntity(entity: Entity): string { + const annotation = + entity.metadata.annotations?.[OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION]; + if (!annotation) { + throw new Error( + `Value for annotation ${OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION} was not found`, + ); + } + + return annotation; +} diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 96b86c6eba..54fd9629ce 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-org-react +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.1.4 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.4-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index f64443b506..42dd6c1f26 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.3", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,7 +36,7 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.9.13", + "@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" @@ -54,7 +54,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index e48a27b900..2fd85644ff 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-org +## 0.6.6-next.0 + +### Patch Changes + +- 36bccb5252: Fix small UI-glitch in MemberListCard +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.6.5 + +### Patch Changes + +- 5a4fa8859b: Updated `LinksGroup` to use `Link` over `ListItem` as this makes the links more obvious and follows the pattern already used in the `GroupProfileCard`. Also updated the `GroupProfileCard` `ExtraDetails` story in Storybook to enable the `showLinks` feature with this off there is no difference between it and the `default` story. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.6.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.6.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.6.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.6.4 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index fa36038045..c1a4e8b1e5 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.6.4", + "version": "0.6.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -56,7 +56,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx index cfb3d847a4..f183ab4d8e 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx @@ -123,7 +123,7 @@ export const ExtraDetails = () => ( <EntityProvider entity={extraDetailsEntity}> <Grid container spacing={4}> <Grid item xs={12} md={4}> - <GroupProfileCard variant="gridItem" /> + <GroupProfileCard variant="gridItem" showLinks /> </Grid> </Grid> </EntityProvider> diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index 7e5b683bf1..c41a73ab9c 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -64,6 +64,7 @@ const useStyles = makeStyles((theme: Theme) => whiteSpace: 'nowrap', textOverflow: 'ellipsis', display: 'inline-block', + verticalAlign: 'middle', maxWidth: '100%', '&:hover': { overflow: 'visible', diff --git a/plugins/org/src/components/Cards/Meta/LinksGroup.tsx b/plugins/org/src/components/Cards/Meta/LinksGroup.tsx index eeade18411..dfc654c14b 100644 --- a/plugins/org/src/components/Cards/Meta/LinksGroup.tsx +++ b/plugins/org/src/components/Cards/Meta/LinksGroup.tsx @@ -23,6 +23,7 @@ import { Divider, } from '@material-ui/core'; import React from 'react'; +import { Link } from '@backstage/core-components'; const WebLink = ({ href, @@ -33,9 +34,11 @@ const WebLink = ({ text?: string; Icon?: IconComponent; }) => ( - <ListItem button component="a" key={href} href={href}> + <ListItem key={href}> <ListItemIcon>{Icon ? <Icon /> : <LanguageIcon />}</ListItemIcon> - <ListItemText>{text}</ListItemText> + <ListItemText> + <Link to={href}>{text}</Link> + </ListItemText> </ListItem> ); diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index 677093d270..a69e2da666 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-pagerduty +## 0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.5.8 + +### Patch Changes + +- 804746dd58: The PagerDutyCard now supports an optional `readOnly` property (`<PagerDutyCard readOnly />`) for suppressing the rendering of the "Create Incident" button from the Backstage UI. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## 0.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.5.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.5.7 ### Patch Changes diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index 7b3124ab0f..ac3ba529dd 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -6,7 +6,7 @@ # How it Works - The Backstage PagerDuty plugin allows PagerDuty information about a Backstage entity to be displayed within Backstage. This includes active incidents, recent change events, as well as the current on-call responders' names, email addresses, and links to their profiles in PagerDuty. -- Incidents can be manually triggered via the plugin with a user-provided description, which will in turn notify the current on-call responders. +- Incidents can be manually triggered via the plugin with a user-provided description, which will in turn notify the current on-call responders (Alternatively, the plugin can be configured with an optional `readOnly` property to suppress the ability to trigger incidents from the plugin). - _Note: This feature is only available when providing the `pagerduty.com/integration-key` annotation_ - Change events will be displayed in a separate tab. If the change event payload has additional links the first link only will be rendered. @@ -16,7 +16,7 @@ # Support -If you need help with this plugin, please reach out on the [Backstage Discord server](https://discord.gg/MUpMjP2). +If you need help with this plugin, please reach out on the [Backstage Discord server](https://discord.gg/backstage-687207715902193673). # Integration Walk-through @@ -145,6 +145,24 @@ pagerduty: eventsBaseUrl: 'https://events.pagerduty.com/v2' ``` +To suppress the rendering of the actionable incident-creation button, the `PagerDutyCard` can also be instantiated in `readOnly` mode: + +```ts +<PagerDuty readOnly /> +``` + +**WARNING**: In current implementation, the PagerDuty plugin requires the `/pagerduty` proxy endpoint be exposed by the Backstage backend as an unprotected endpoint, in effect enabling PagerDuty API access using the configured `PAGERDUTY_TOKEN` for any user or process with access to the `/pagerduty` Backstage backend endpoint. If you regard this as problematic, consider using the plugin in `readOnly` mode (`<PagerDutyCard readOnly />`) using the following proxy configuration: + +```yaml +proxy: + '/pagerduty': + target: https://api.pagerduty.com + headers: + Authorization: Token token=${PAGERDUTY_TOKEN} + # prohibit the `/pagerduty` proxy endpoint from servicing non-GET requests + allowedMethods: ['GET'] +``` + # How to Uninstall 1. Remove any configuration added in Backstage yaml files, such as the proxy configuration in `app-config.yaml` and the integration key in an entity's annotations. diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index 5bfe84e63f..b460086976 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -14,7 +14,7 @@ import { FetchApi } from '@backstage/core-plugin-api'; import { ReactNode } from 'react'; // @public (undocumented) -export const EntityPagerDutyCard: () => JSX.Element; +export const EntityPagerDutyCard: (props: PagerDutyCardProps) => JSX.Element; // @public (undocumented) const isPluginApplicableToEntity: (entity: Entity) => boolean; @@ -45,7 +45,12 @@ export type PagerDutyAssignee = { }; // @public (undocumented) -export const PagerDutyCard: () => JSX.Element; +export const PagerDutyCard: (props: PagerDutyCardProps) => JSX.Element; + +// @public (undocumented) +export type PagerDutyCardProps = { + readOnly?: boolean; +}; // @public (undocumented) export type PagerDutyChangeEvent = { diff --git a/plugins/pagerduty/dev/index.tsx b/plugins/pagerduty/dev/index.tsx index 676eb687a4..e45e0ffd8f 100644 --- a/plugins/pagerduty/dev/index.tsx +++ b/plugins/pagerduty/dev/index.tsx @@ -13,7 +13,153 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createDevApp } from '@backstage/dev-utils'; -import { pagerDutyPlugin } from '../src/plugin'; -createDevApp().registerPlugin(pagerDutyPlugin).render(); +import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { createDevApp } from '@backstage/dev-utils'; +import { pagerDutyPlugin, EntityPagerDutyCard } from '../src/plugin'; +import { pagerDutyApiRef } from '../src/api'; +import { PagerDutyApi, PagerDutyTriggerAlarmRequest } from '../src/api/types'; +import { + PagerDutyIncident, + PagerDutyChangeEvent, +} from '../src/components/types'; + +const mockEntity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'backstage', + description: 'backstage.io', + annotations: { + 'github.com/project-slug': 'backstage/backstage', + 'pagerduty.com/service-id': 'foo', + 'pagerduty.com/integration-key': 'foo', + }, + }, + spec: { + lifecycle: 'production', + type: 'website', + owner: 'user:guest', + }, +}; + +const mockPagerDutyApi: PagerDutyApi = { + async getServiceByEntity(entity: Entity) { + return { + service: { + name: entity.metadata.name, + integrationKey: 'key', + id: '123', + html_url: 'http://service', + escalation_policy: { + id: '123', + html_url: 'http://escalationpolicy', + user: { + id: '123', + summary: 'summary', + email: 'email@email.com', + html_url: 'http://user', + name: 'some-user', + }, + }, + }, + }; + }, + + async getIncidentsByServiceId(serviceId: string) { + const incident = (title: string) => { + return { + id: '123', + title: title, + status: 'acknowledged', + html_url: 'http://incident', + assignments: [ + { + assignee: { + id: '123', + summary: 'Jane Doe', + html_url: 'http://assignee', + }, + }, + ], + serviceId: serviceId, + created_at: '2015-10-06T21:30:42Z', + } as PagerDutyIncident; + }; + + return { + incidents: [ + incident('Some Alerting Incident'), + incident('Another Alerting Incident'), + ], + }; + }, + + async getChangeEventsByServiceId(serviceId: string) { + const changeEvent = (description: string) => { + return { + id: serviceId, + source: 'some-source', + html_url: 'http://changeevent', + links: [ + { + href: 'http://link', + text: 'link text', + }, + ], + summary: description, + timestamp: '2018-10-06T21:30:42Z', + } as PagerDutyChangeEvent; + }; + + return { + change_events: [ + changeEvent('us-east-1 deployment'), + changeEvent('us-west-2 deployment'), + ], + }; + }, + + async getOnCallByPolicyId() { + const oncall = (name: string, escalation: number) => { + return { + user: { + id: '123', + name: name, + html_url: 'http://assignee', + summary: 'summary', + email: 'email@email.com', + }, + escalation_level: escalation, + }; + }; + + return { + oncalls: [oncall('Jane Doe', 1), oncall('John Doe', 2)], + }; + }, + + async triggerAlarm(request: PagerDutyTriggerAlarmRequest) { + return new Response(request.description); + }, +}; + +createDevApp() + .registerApi({ + api: pagerDutyApiRef, + deps: {}, + factory: () => mockPagerDutyApi, + }) + .registerPlugin(pagerDutyPlugin) + .addPage({ + path: '/pagerduty', + title: 'PagerDuty', + element: ( + <EntityProvider entity={mockEntity}> + <EntityPagerDutyCard /> + </EntityProvider> + ), + }) + .render(); diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index fceead836b..f248c6f31f 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.5.7", + "version": "0.5.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -61,7 +61,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index 85202a9cfc..34c8914fde 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -356,4 +356,27 @@ describe('PageDutyCard', () => { expect(getByText('Empty escalation policy')).toBeInTheDocument(); }); }); + + describe('when entity has all annotations but the plugin has been configured to be "read only"', () => { + it('queries by integration key but does not render the "Create Incident" button', async () => { + mockPagerDutyApi.getServiceByEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + <ApiProvider apis={apis}> + <EntityProvider entity={entityWithAllAnnotations}> + <PagerDutyCard readOnly /> + </EntityProvider> + </ApiProvider>, + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + expect(() => getByText('Create Incident')).toThrow(); + }); + }); }); diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index 15967fae01..fd23166d88 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -54,7 +54,13 @@ export const isPluginApplicableToEntity = (entity: Entity) => ); /** @public */ -export const PagerDutyCard = () => { +export type PagerDutyCardProps = { + readOnly?: boolean; +}; + +/** @public */ +export const PagerDutyCard = (props: PagerDutyCardProps) => { + const { readOnly } = props; const { entity } = useEntity(); const pagerDutyEntity = getPagerDutyEntity(entity); const api = useApi(pagerDutyApiRef); @@ -156,7 +162,11 @@ export const PagerDutyCard = () => { title="PagerDuty" subheader={ <HeaderIconLinkRow - links={[serviceLink, triggerLink, escalationPolicyLink]} + links={ + !readOnly + ? [serviceLink, triggerLink, escalationPolicyLink] + : [serviceLink, escalationPolicyLink] + } /> } /> diff --git a/plugins/pagerduty/src/components/index.ts b/plugins/pagerduty/src/components/index.ts index 17bb2c03dc..e7c1ff4e72 100644 --- a/plugins/pagerduty/src/components/index.ts +++ b/plugins/pagerduty/src/components/index.ts @@ -23,6 +23,8 @@ export type { PagerDutyUser, } from './types'; +export type { PagerDutyCardProps } from './PagerDutyCard'; + export { isPluginApplicableToEntity, isPluginApplicableToEntity as isPagerDutyAvailable, diff --git a/plugins/periskop-backend/CHANGELOG.md b/plugins/periskop-backend/CHANGELOG.md index 527139a776..55769cb201 100644 --- a/plugins/periskop-backend/CHANGELOG.md +++ b/plugins/periskop-backend/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-periskop-backend +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + +## 0.1.13 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + +## 0.1.13-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.1.11 ### Patch Changes diff --git a/plugins/periskop-backend/api-report.md b/plugins/periskop-backend/api-report.md index caaed109f9..7906bc4785 100644 --- a/plugins/periskop-backend/api-report.md +++ b/plugins/periskop-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 { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -10,6 +11,9 @@ import { Logger } from 'winston'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise<express.Router>; +// @alpha +export const periskopPlugin: () => BackendFeature; + // @public (undocumented) export interface RouterOptions { // (undocumented) diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index 670345ff27..0472be43b0 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop-backend", - "version": "0.1.11", + "version": "0.1.14-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,11 +11,12 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -24,6 +25,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@types/express": "*", "express": "^4.17.1", @@ -35,10 +37,11 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.6" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/periskop-backend/src/index.ts b/plugins/periskop-backend/src/index.ts index ca73cb27ba..a053633745 100644 --- a/plugins/periskop-backend/src/index.ts +++ b/plugins/periskop-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export { periskopPlugin } from './plugin'; diff --git a/plugins/periskop-backend/src/plugin.ts b/plugins/periskop-backend/src/plugin.ts new file mode 100644 index 0000000000..933c864d50 --- /dev/null +++ b/plugins/periskop-backend/src/plugin.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The Periskop backend plugin. + * + * @alpha + */ +export const periskopPlugin = createBackendPlugin({ + pluginId: 'periskop', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ config, logger, httpRouter }) { + httpRouter.use( + await createRouter({ config, logger: loggerToWinstonLogger(logger) }), + ); + }, + }); + }, +}); diff --git a/plugins/periskop/CHANGELOG.md b/plugins/periskop/CHANGELOG.md index 640e5e1631..1a700f5aa0 100644 --- a/plugins/periskop/CHANGELOG.md +++ b/plugins/periskop/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-periskop +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## 0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.12 ### Patch Changes diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 61b85a4a74..eba10dc41e 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop", - "version": "0.1.12", + "version": "0.1.14-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -50,7 +50,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 35a0a54fb6..87be1e49e1 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-permission-backend +## 0.5.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + +## 0.5.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + +## 0.5.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## 0.5.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + +## 0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + ## 0.5.15 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 98255fcdfc..eaa2f6ddd1 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.15", + "version": "0.5.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,7 +42,7 @@ "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 472a86aaab..960ca249d3 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -50,6 +50,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "msw": "^0.49.0" + "msw": "^1.0.0" } } diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index a66ab38e98..8d2789e615 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/plugin-permission-node +## 0.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + +## 0.7.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + +## 0.7.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## 0.7.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## 0.7.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + ## 0.7.3 ### Patch Changes diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 3f712fe362..f89ff0a8d5 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -112,20 +112,33 @@ export const createConditionTransformer: < ) => ConditionTransformer<TQuery>; // @public -export const createPermissionIntegrationRouter: < +export function createPermissionIntegrationRouter< TResourceType extends string, TResource, ->(options: { +>( + options: CreatePermissionIntegrationRouterResourceOptions< + TResourceType, + TResource + >, +): express.Router; + +// @public +export function createPermissionIntegrationRouter(options: { + permissions: Array<Permission>; +}): express.Router; + +// @public +export type CreatePermissionIntegrationRouterResourceOptions< + TResourceType extends string, + TResource, +> = { resourceType: TResourceType; - permissions?: Permission[] | undefined; - rules: PermissionRule< - TResource, - any, - NoInfer<TResourceType>, - PermissionRuleParams - >[]; - getResources: (resourceRefs: string[]) => Promise<(TResource | undefined)[]>; -}) => express.Router; + permissions?: Array<Permission>; + rules: PermissionRule<TResource, any, NoInfer<TResourceType>>[]; + getResources?: ( + resourceRefs: string[], + ) => Promise<Array<TResource | undefined>>; +}; // @public export const createPermissionRule: < @@ -137,17 +150,17 @@ export const createPermissionRule: < rule: PermissionRule<TResource, TQuery, TResourceType, TParams>, ) => PermissionRule<TResource, TQuery, TResourceType, TParams>; -// @alpha +// @public export const isAndCriteria: <T>( criteria: PermissionCriteria<T>, ) => criteria is AllOfCriteria<T>; -// @alpha +// @public export const isNotCriteria: <T>( criteria: PermissionCriteria<T>, ) => criteria is NotCriteria<T>; -// @alpha +// @public export const isOrCriteria: <T>( criteria: PermissionCriteria<T>, ) => criteria is AnyOfCriteria<T>; diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index c5cf2f9ce4..673250cd19 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.7.3", + "version": "0.7.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -48,7 +48,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 32d814465f..1a7462665e 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -19,18 +19,15 @@ import { createPermission, Permission, } from '@backstage/plugin-permission-common'; -import express, { Express, Router } from 'express'; +import express from 'express'; import request, { Response } from 'supertest'; import { z } from 'zod'; -import { createPermissionIntegrationRouter } from './createPermissionIntegrationRouter'; +import { + createPermissionIntegrationRouter, + CreatePermissionIntegrationRouterResourceOptions, +} from './createPermissionIntegrationRouter'; import { createPermissionRule } from './createPermissionRule'; -const mockGetResources: jest.MockedFunction< - Parameters<typeof createPermissionIntegrationRouter>[0]['getResources'] -> = jest.fn(async resourceRefs => - resourceRefs.map(resourceRef => ({ id: resourceRef })), -); - const testPermission: Permission = createPermission({ name: 'test.permission', attributes: {}, @@ -56,29 +53,35 @@ const testRule2 = createPermissionRule({ toQuery: () => ({}), }); +const defaultMockedGetResources: CreatePermissionIntegrationRouterResourceOptions< + string, + { id: string } +>['getResources'] = jest.fn(async resourceRefs => + resourceRefs.map(resourceRef => ({ id: resourceRef })), +); + +const createApp = ( + mockedGetResources: + | typeof defaultMockedGetResources + | null = defaultMockedGetResources, +) => { + const router = mockedGetResources + ? createPermissionIntegrationRouter({ + resourceType: 'test-resource', + permissions: [testPermission], + getResources: mockedGetResources, + rules: [testRule1, testRule2], + }) + : createPermissionIntegrationRouter({ permissions: [testPermission] }); + + return express().use(router); +}; + describe('createPermissionIntegrationRouter', () => { - let app: Express; - let router: Router; - - beforeAll(() => { - router = createPermissionIntegrationRouter({ - resourceType: 'test-resource', - permissions: [testPermission], - getResources: mockGetResources, - rules: [testRule1, testRule2], - }); - - app = express().use(router); - }); - afterEach(() => { jest.clearAllMocks(); }); - it('works', async () => { - expect(router).toBeDefined(); - }); - describe('POST /.well-known/backstage/permissions/apply-conditions', () => { it.each([ { @@ -150,7 +153,7 @@ describe('createPermissionIntegrationRouter', () => { ], }, ])('returns 200/ALLOW when criteria match (case %#)', async conditions => { - const response = await request(app) + const response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -238,7 +241,7 @@ describe('createPermissionIntegrationRouter', () => { ])( 'returns 200/DENY when criteria do not match (case %#)', async conditions => { - const response = await request(app) + const response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -262,7 +265,7 @@ describe('createPermissionIntegrationRouter', () => { let response: Response; beforeEach(async () => { - response = await request(app) + response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -353,7 +356,7 @@ describe('createPermissionIntegrationRouter', () => { }); it('calls getResources for all required resources at once', () => { - expect(mockGetResources).toHaveBeenCalledWith([ + expect(defaultMockedGetResources).toHaveBeenCalledWith([ 'default:test/resource-1', 'default:test/resource-2', 'default:test/resource-3', @@ -363,7 +366,7 @@ describe('createPermissionIntegrationRouter', () => { }); it('returns 400 when called with incorrect resource type', async () => { - const response = await request(app) + const response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -413,11 +416,14 @@ describe('createPermissionIntegrationRouter', () => { }); it('returns 200/DENY when resource is not found', async () => { - mockGetResources.mockImplementationOnce(async resourceRefs => + const mockedGetResources: CreatePermissionIntegrationRouterResourceOptions< + string, + { id: string } + >['getResources'] = jest.fn(async resourceRefs => resourceRefs.map(() => undefined), ); - const response = await request(app) + const response = await request(createApp(mockedGetResources)) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -446,7 +452,10 @@ describe('createPermissionIntegrationRouter', () => { }); it('interleaves responses for present and missing resources', async () => { - mockGetResources.mockImplementationOnce(async resourceRefs => + const mockedGetResources: CreatePermissionIntegrationRouterResourceOptions< + string, + { id: string } + >['getResources'] = jest.fn(async resourceRefs => resourceRefs.map(resourceRef => resourceRef === 'default:test/missing-resource' ? undefined @@ -454,7 +463,7 @@ describe('createPermissionIntegrationRouter', () => { ), ); - const response = await request(app) + const response = await request(createApp(mockedGetResources)) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -548,18 +557,31 @@ describe('createPermissionIntegrationRouter', () => { ], }, ])(`returns 400 for invalid input %#`, async input => { - const response = await request(app) + const response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send(input); expect(response.status).toEqual(400); expect(response.error && response.error.text).toMatch(/invalid/i); }); + + it('returns 501 with no getResources implementation', async () => { + const response = await request(createApp(null)) + .post('/.well-known/backstage/permissions/apply-conditions') + .send({ + items: [], + }); + + expect(response.status).toEqual(501); + expect(response.body.error.message).toEqual( + `This plugin does not expose any permission rule or can't evaluate conditional decisions`, + ); + }); }); describe('GET /.well-known/backstage/permissions/metadata', () => { it('returns a list of permissions and rules used by a given backend', async () => { - const response = await request(app).get( + const response = await request(createApp()).get( '/.well-known/backstage/permissions/metadata', ); diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 23b59c9c43..087de23198 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -36,6 +36,7 @@ import { isNotCriteria, isOrCriteria, } from './util'; +import { NotImplementedError } from '@backstage/errors'; const permissionCriteriaSchema: z.ZodSchema< PermissionCriteria<PermissionCondition> @@ -159,6 +160,28 @@ const applyConditions = <TResourceType extends string, TResource>( return rule.apply(resource, criteria.params ?? {}); }; +/** + * Options for creating a permission integration router specific + * for a particular resource type. + * + * @public + */ +export type CreatePermissionIntegrationRouterResourceOptions< + TResourceType extends string, + TResource, +> = { + resourceType: TResourceType; + permissions?: Array<Permission>; + // Do not infer value of TResourceType from supplied rules. + // instead only consider the resourceType parameter, and + // consider any rules whose resource type does not match + // to be an error. + rules: PermissionRule<TResource, any, NoInfer<TResourceType>>[]; + getResources?: ( + resourceRefs: string[], + ) => Promise<Array<TResource | undefined>>; +}; + /** * Create an express Router which provides an authorization route to allow * integration between the permission backend and other Backstage backend @@ -166,6 +189,9 @@ const applyConditions = <TResourceType extends string, TResource>( * their resources should add the router created by this function to their * express app inside their `createRouter` implementation. * + * In case the `permissions` option is provided, the router also + * provides a route that exposes permissions and routes of a plugin. + * * @remarks * * To make this concrete, we can use the Backstage software catalog as an @@ -194,25 +220,44 @@ const applyConditions = <TResourceType extends string, TResource>( * * @public */ -export const createPermissionIntegrationRouter = < +export function createPermissionIntegrationRouter< TResourceType extends string, TResource, ->(options: { - resourceType: TResourceType; - permissions?: Array<Permission>; - // Do not infer value of TResourceType from supplied rules. - // instead only consider the resourceType parameter, and - // consider any rules whose resource type does not match - // to be an error. - rules: PermissionRule<TResource, any, NoInfer<TResourceType>>[]; - getResources: ( - resourceRefs: string[], - ) => Promise<Array<TResource | undefined>>; -}): express.Router => { - const { resourceType, permissions, rules, getResources } = options; +>( + options: CreatePermissionIntegrationRouterResourceOptions< + TResourceType, + TResource + >, +): express.Router; + +/** + * + * Create an express Router which provides a route that exposes + * permissions and routes of a plugin. + * @public + */ +export function createPermissionIntegrationRouter(options: { + permissions: Array<Permission>; +}): express.Router; + +/** + * @public + */ +export function createPermissionIntegrationRouter< + TResourceType extends string, + TResource, +>( + options: + | { permissions: Array<Permission> } + | CreatePermissionIntegrationRouterResourceOptions< + TResourceType, + TResource + >, +): express.Router { const router = Router(); router.use(express.json()); + const { permissions = [], rules = [] } = { rules: [], ...options }; router.get('/.well-known/backstage/permissions/metadata', (_, res) => { const serializedRules: MetadataResponseSerializedRule[] = rules.map( rule => ({ @@ -231,25 +276,35 @@ export const createPermissionIntegrationRouter = < return res.json(responseJson); }); - const getRule = createGetRule(rules); - - const assertValidResourceTypes = ( - requests: ApplyConditionsRequestEntry[], - ) => { - const invalidResourceTypes = requests - .filter(request => request.resourceType !== resourceType) - .map(request => request.resourceType); - - if (invalidResourceTypes.length) { - throw new InputError( - `Unexpected resource types: ${invalidResourceTypes.join(', ')}.`, - ); - } - }; - router.post( '/.well-known/backstage/permissions/apply-conditions', async (req, res: Response<ApplyConditionsResponse | string>) => { + if ( + !isCreatePermissionIntegrationRouterResourceOptions(options) || + options.getResources === undefined + ) { + throw new NotImplementedError( + `This plugin does not expose any permission rule or can't evaluate conditional decisions`, + ); + } + const { resourceType, getResources } = options; + + const getRule = createGetRule(rules); + + const assertValidResourceTypes = ( + requests: ApplyConditionsRequestEntry[], + ) => { + const invalidResourceTypes = requests + .filter(request => request.resourceType !== resourceType) + .map(request => request.resourceType); + + if (invalidResourceTypes.length) { + throw new InputError( + `Unexpected resource types: ${invalidResourceTypes.join(', ')}.`, + ); + } + }; + const parseResult = applyConditionsRequestSchema.safeParse(req.body); if (!parseResult.success) { @@ -288,4 +343,28 @@ export const createPermissionIntegrationRouter = < router.use(errorHandler()); return router; -}; +} + +function isCreatePermissionIntegrationRouterResourceOptions< + TResourceType extends string, + TResource, +>( + options: + | { permissions: Array<Permission> } + | CreatePermissionIntegrationRouterResourceOptions< + TResourceType, + TResource + >, +): options is CreatePermissionIntegrationRouterResourceOptions< + TResourceType, + TResource +> { + return ( + ( + options as CreatePermissionIntegrationRouterResourceOptions< + TResourceType, + TResource + > + ).resourceType !== undefined + ); +} diff --git a/plugins/permission-node/src/integration/util.ts b/plugins/permission-node/src/integration/util.ts index e448068e17..9be90e15fc 100644 --- a/plugins/permission-node/src/integration/util.ts +++ b/plugins/permission-node/src/integration/util.ts @@ -33,7 +33,7 @@ export type NoInfer<T> = T extends infer S ? S : never; /** * Utility function used to parse a PermissionCriteria * @param criteria - a PermissionCriteria - * @alpha + * @public * * @returns `true` if the permission criteria is of type allOf, * narrowing down `criteria` to the specific type. @@ -46,7 +46,7 @@ export const isAndCriteria = <T>( /** * Utility function used to parse a PermissionCriteria of type * @param criteria - a PermissionCriteria - * @alpha + * @public * * @returns `true` if the permission criteria is of type anyOf, * narrowing down `criteria` to the specific type. @@ -59,7 +59,7 @@ export const isOrCriteria = <T>( /** * Utility function used to parse a PermissionCriteria * @param criteria - a PermissionCriteria - * @alpha + * @public * * @returns `true` if the permission criteria is of type not, * narrowing down `criteria` to the specific type. diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 4093b6a430..914552da32 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-permission-react +## 0.4.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-permission-common@0.7.3 + +## 0.4.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/plugin-permission-common@0.7.3 + ## 0.4.9 ### Patch Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index beef8e093d..dcf45270f4 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.4.9", + "version": "0.4.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/playlist-backend/CHANGELOG.md b/plugins/playlist-backend/CHANGELOG.md index 99c1dc0ea4..9dcf4493ea 100644 --- a/plugins/playlist-backend/CHANGELOG.md +++ b/plugins/playlist-backend/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-playlist-backend +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + - @backstage/plugin-playlist-common@0.1.4 + +## 0.2.5 + +### Patch Changes + +- b977c2e69f: Minor improvements to the descriptions provided with permission rules schemas +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + - @backstage/plugin-playlist-common@0.1.4 + +## 0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-playlist-common@0.1.4 + +## 0.2.5-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + - @backstage/plugin-playlist-common@0.1.4 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + ## 0.2.3 ### Patch Changes diff --git a/plugins/playlist-backend/package.json b/plugins/playlist-backend/package.json index 28d5907925..b4232ff786 100644 --- a/plugins/playlist-backend/package.json +++ b/plugins/playlist-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist-backend", - "version": "0.2.3", + "version": "0.2.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,7 +23,6 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", @@ -43,9 +42,10 @@ "zod": "~3.18.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/playlist-backend/src/permissions/rules.ts b/plugins/playlist-backend/src/permissions/rules.ts index 99f4725972..e29465ed0f 100644 --- a/plugins/playlist-backend/src/permissions/rules.ts +++ b/plugins/playlist-backend/src/permissions/rules.ts @@ -31,10 +31,10 @@ const createPlaylistPermissionRule = makeCreatePermissionRule< const isOwner = createPlaylistPermissionRule({ name: 'IS_OWNER', - description: 'Should allow only if the playlist belongs to the user', + description: 'Allow playlists owned by the given entity refs', resourceType: PLAYLIST_LIST_RESOURCE_TYPE, paramsSchema: z.object({ - owners: z.array(z.string()).describe('List of owner entity refs'), + owners: z.array(z.string()).describe('List of entity refs to match on'), }), apply: (list: PlaylistMetadata, { owners }) => owners.includes(list.owner), toQuery: ({ owners }) => ({ @@ -45,7 +45,7 @@ const isOwner = createPlaylistPermissionRule({ const isPublic = createPlaylistPermissionRule({ name: 'IS_PUBLIC', - description: 'Should allow only if the playlist is public', + description: 'Allow playlists that are set as public', resourceType: PLAYLIST_LIST_RESOURCE_TYPE, apply: (list: PlaylistMetadata) => list.public, toQuery: () => ({ key: 'public', values: [true] }), diff --git a/plugins/playlist-backend/src/service/ListPlaylistsFilter.ts b/plugins/playlist-backend/src/service/ListPlaylistsFilter.ts index a746286136..680f389f8c 100644 --- a/plugins/playlist-backend/src/service/ListPlaylistsFilter.ts +++ b/plugins/playlist-backend/src/service/ListPlaylistsFilter.ts @@ -79,9 +79,9 @@ export function parseListPlaylistsFilterString( const equalsIndex = statement.indexOf('='); const key = - equalsIndex === -1 ? statement : statement.substr(0, equalsIndex).trim(); + equalsIndex === -1 ? statement : statement.slice(0, equalsIndex).trim(); const value = - equalsIndex === -1 ? undefined : statement.substr(equalsIndex + 1).trim(); + equalsIndex === -1 ? undefined : statement.slice(equalsIndex + 1).trim(); if (!key || !value) { throw new InputError( diff --git a/plugins/playlist/CHANGELOG.md b/plugins/playlist/CHANGELOG.md index b44a4b9b55..e6d1a1d809 100644 --- a/plugins/playlist/CHANGELOG.md +++ b/plugins/playlist/CHANGELOG.md @@ -1,5 +1,85 @@ # @backstage/plugin-playlist +## 0.1.7-next.0 + +### Patch Changes + +- 122a37a880: Internal update to work with newer versions of react-router +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.11-next.0 + - @backstage/plugin-playlist-common@0.1.4 + - @backstage/plugin-search-react@1.5.1-next.0 + +## 0.1.6 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.10 + - @backstage/plugin-playlist-common@0.1.4 + +## 0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist-common@0.1.4 + +## 0.1.6-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist-common@0.1.4 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + ## 0.1.5 ### Patch Changes diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json index 4343de503a..0cf67652f3 100644 --- a/plugins/playlist/package.json +++ b/plugins/playlist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist", - "version": "0.1.5", + "version": "0.1.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,7 +38,7 @@ "@backstage/plugin-playlist-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.9.13", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.57", "lodash": "^4.17.21", @@ -61,7 +61,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.49.0", + "msw": "^1.0.0", "swr": "^2.0.0" }, "files": [ diff --git a/plugins/playlist/src/components/PlaylistCard/PlaylistCard.tsx b/plugins/playlist/src/components/PlaylistCard/PlaylistCard.tsx index 2db7e85f7e..1bf698f6be 100644 --- a/plugins/playlist/src/components/PlaylistCard/PlaylistCard.tsx +++ b/plugins/playlist/src/components/PlaylistCard/PlaylistCard.tsx @@ -15,8 +15,8 @@ */ import { - Button, ItemCardHeader, + LinkButton, MarkdownContent, } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; @@ -118,13 +118,13 @@ export const PlaylistCard = ({ playlist }: PlaylistCardProps) => { </Box> </CardContent> <CardActions> - <Button + <LinkButton color="primary" aria-label={`Choose ${playlist.name}`} to={playlistRoute({ playlistId: playlist.id })} > Choose - </Button> + </LinkButton> </CardActions> </Card> ); diff --git a/plugins/playlist/src/components/PlaylistPage/AddEntitiesDrawer.tsx b/plugins/playlist/src/components/PlaylistPage/AddEntitiesDrawer.tsx index 05dd4da231..fe08870bcd 100644 --- a/plugins/playlist/src/components/PlaylistPage/AddEntitiesDrawer.tsx +++ b/plugins/playlist/src/components/PlaylistPage/AddEntitiesDrawer.tsx @@ -102,12 +102,16 @@ export const AddEntitiesDrawer = ({ const entityRoute = useRouteRef(entityRouteRef); const entityLocationRegex = useMemo(() => { const forwardSlashRegex = new RegExp('/', 'g'); + // We need to do this in two steps, since entityRoute will ruin some special chars, like question marks const locationRegex = entityRoute({ - namespace: '(?<namespace>.+?)', - kind: '(?<kind>.+?)', - name: '(?<name>.+?)', - }).replace(forwardSlashRegex, '\\/'); - + namespace: '__REPLACE_WITH_NAMESPACE__', + kind: '__REPLACE_WITH_KIND__', + name: '__REPLACE_WITH_NAME__', + }) + .replace('__REPLACE_WITH_NAMESPACE__', '(?<namespace>.+?)') + .replace('__REPLACE_WITH_KIND__', '(?<kind>.+?)') + .replace('__REPLACE_WITH_NAME__', '(?<name>.+?)') + .replace(forwardSlashRegex, '\\/'); return new RegExp(`${locationRegex}$`); }, [entityRoute]); @@ -127,12 +131,12 @@ export const AddEntitiesDrawer = ({ const addEntity = useCallback( entityResult => { - // TODO (kuangp): this parsing of the location is not great. Ideally `CatalogEntityDocument` + // TODO(kuangp): this parsing of the location is not great. Ideally `CatalogEntityDocument` // contains the `metadata.name` field so we can derive the full ref and we only fall back to // parsing location if it's missing (ie. for older versions) - const { groups } = entityResult.location.match(entityLocationRegex); - if (groups) { - onAdd(stringifyEntityRef(groups)); + const match = entityResult.location.match(entityLocationRegex); + if (match?.groups) { + onAdd(stringifyEntityRef(match?.groups)); } else { // eslint-disable-next-line no-console console.error( diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 491c9e4f99..2f5c46445a 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-proxy-backend +## 0.2.37-next.0 + +### Patch Changes + +- 3e3eea4bc7: The proxy-backend plugin now supports reviving request bodies that have previously been consumed by an express middleware (e.g. `express.json()`). This is done by setting `reviveConsumedRequestBodies: true` on the proxy `RouterOptions`. In order to preserve the current behavior, the proxy will **not** revive request bodies by default. + + The following is an example of a proxy `createRouter` invocation that revives request bodies: + + ```diff + const router = await createRouter({ + config, + logger, + discovery, + + reviveConsumedRequestBodies: true, + }); + ``` + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + +## 0.2.36 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + +## 0.2.36-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## 0.2.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + +## 0.2.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.2.34 ### Patch Changes diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md index 581e7a904c..12d799bdf7 100644 --- a/plugins/proxy-backend/api-report.md +++ b/plugins/proxy-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 { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -11,6 +12,16 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; // @public export function createRouter(options: RouterOptions): Promise<express.Router>; +// @alpha +export const proxyPlugin: ( + options?: + | { + skipInvalidProxies?: boolean | undefined; + reviveConsumedRequestBodies?: boolean | undefined; + } + | undefined, +) => BackendFeature; + // @public (undocumented) export interface RouterOptions { // (undocumented) @@ -20,6 +31,8 @@ export interface RouterOptions { // (undocumented) logger: Logger; // (undocumented) + reviveConsumedRequestBodies?: boolean; + // (undocumented) skipInvalidProxies?: boolean; } ``` diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 52e021b7c9..a08900fd83 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,14 +1,15 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.2.34", + "version": "0.2.37-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "backend-plugin" @@ -24,7 +25,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -33,6 +34,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -51,12 +53,13 @@ "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", "@types/yup": "^0.29.13", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ "dist", - "config.d.ts" + "config.d.ts", + "alpha" ], "configSchema": "config.d.ts" } diff --git a/plugins/proxy-backend/src/index.ts b/plugins/proxy-backend/src/index.ts index 8f5d5c79dc..2658a557b7 100644 --- a/plugins/proxy-backend/src/index.ts +++ b/plugins/proxy-backend/src/index.ts @@ -21,3 +21,4 @@ */ export * from './service'; +export { proxyPlugin } from './plugin'; diff --git a/plugins/proxy-backend/src/plugin.ts b/plugins/proxy-backend/src/plugin.ts new file mode 100644 index 0000000000..66eeb4a7b2 --- /dev/null +++ b/plugins/proxy-backend/src/plugin.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + createBackendPlugin, + coreServices, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The proxy backend plugin. + * + * @alpha + */ +export const proxyPlugin = createBackendPlugin( + (options?: { + skipInvalidProxies?: boolean; + reviveConsumedRequestBodies?: boolean; + }) => ({ + pluginId: 'proxy', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + discovery: coreServices.discovery, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ config, discovery, logger, httpRouter }) { + httpRouter.use( + await createRouter({ + config, + discovery, + logger: loggerToWinstonLogger(logger), + skipInvalidProxies: options?.skipInvalidProxies, + reviveConsumedRequestBodies: options?.reviveConsumedRequestBodies, + }), + ); + }, + }); + }, + }), +); diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index afe78aebeb..9e8c909d25 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -18,11 +18,16 @@ import { getVoidLogger, SingleHostDiscovery } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { Request, Response } from 'express'; import * as http from 'http'; -import { createProxyMiddleware, Options } from 'http-proxy-middleware'; +import { + createProxyMiddleware, + fixRequestBody, + Options, +} from 'http-proxy-middleware'; import { buildMiddleware, createRouter } from './router'; jest.mock('http-proxy-middleware', () => ({ createProxyMiddleware: jest.fn(() => () => undefined), + fixRequestBody: jest.fn(), })); const mockCreateProxyMiddleware = createProxyMiddleware as jest.MockedFunction< @@ -31,25 +36,30 @@ const mockCreateProxyMiddleware = createProxyMiddleware as jest.MockedFunction< describe('createRouter', () => { describe('where all proxy config are valid', () => { + const logger = getVoidLogger(); + const config = new ConfigReader({ + backend: { + baseUrl: 'https://example.com:7007', + listen: { + port: 7007, + }, + }, + proxy: { + '/test': { + target: 'https://example.com', + headers: { + Authorization: 'Bearer supersecret', + }, + }, + }, + }); + const discovery = SingleHostDiscovery.fromConfig(config); + + beforeEach(() => { + mockCreateProxyMiddleware.mockClear(); + }); + it('works', async () => { - const logger = getVoidLogger(); - const config = new ConfigReader({ - backend: { - baseUrl: 'https://example.com:7007', - listen: { - port: 7007, - }, - }, - proxy: { - '/test': { - target: 'https://example.com', - headers: { - Authorization: 'Bearer supersecret', - }, - }, - }, - }); - const discovery = SingleHostDiscovery.fromConfig(config); const router = await createRouter({ config, logger, @@ -57,6 +67,36 @@ describe('createRouter', () => { }); expect(router).toBeDefined(); }); + + it('revives request bodies when set', async () => { + const router = await createRouter({ + config, + logger, + discovery, + reviveConsumedRequestBodies: true, + }); + expect(router).toBeDefined(); + + expect( + mockCreateProxyMiddleware.mock.calls[0][1]?.onProxyReq, + ).toBeDefined(); + expect(mockCreateProxyMiddleware.mock.calls[0][1]?.onProxyReq).toEqual( + fixRequestBody, + ); + }); + + it('does not revive request bodies when not set', async () => { + const router = await createRouter({ + config, + logger, + discovery, + }); + expect(router).toBeDefined(); + + expect( + mockCreateProxyMiddleware.mock.calls[0][1]?.onProxyReq, + ).not.toBeDefined(); + }); }); describe('where buildMiddleware would fail', () => { @@ -389,6 +429,46 @@ describe('buildMiddleware', () => { expect(Object.keys(testClientResponse.headers!)).toEqual(['set-cookie']); }); + it('revives request body when configured', async () => { + buildMiddleware( + '/proxy', + logger, + '/test', + { + target: 'http://mocked', + }, + true, + ); + + expect(createProxyMiddleware).toHaveBeenCalledTimes(1); + + const config = mockCreateProxyMiddleware.mock.calls[0][1] as Options; + + expect(config).toBeDefined(); + expect(config.onProxyReq).toBeDefined(); + + config.onProxyReq!( + {} as http.ClientRequest, + {} as Request, + {} as Response, + {}, + ); + expect(fixRequestBody).toHaveBeenCalledTimes(1); + }); + + it('does not revive request body when not configured', async () => { + buildMiddleware('/proxy', logger, '/test', { + target: 'http://mocked', + }); + + expect(createProxyMiddleware).toHaveBeenCalledTimes(1); + + const config = mockCreateProxyMiddleware.mock.calls[0][1] as Options; + + expect(config).toBeDefined(); + expect(config.onProxyReq).not.toBeDefined(); + }); + it('rejects malformed target URLs', async () => { expect(() => buildMiddleware('/proxy', logger, '/test', 'backstage.io'), diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index 224c2c6202..b669e82f3f 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -19,6 +19,7 @@ import express from 'express'; import Router from 'express-promise-router'; import { createProxyMiddleware, + fixRequestBody, Options, RequestHandler, } from 'http-proxy-middleware'; @@ -53,11 +54,13 @@ export interface RouterOptions { config: Config; discovery: PluginEndpointDiscovery; skipInvalidProxies?: boolean; + reviveConsumedRequestBodies?: boolean; } export interface ProxyConfig extends Options { allowedMethods?: string[]; allowedHeaders?: string[]; + reviveRequestBody?: boolean; } // Creates a proxy middleware, possibly with defaults added on top of the @@ -67,6 +70,7 @@ export function buildMiddleware( logger: Logger, route: string, config: string | ProxyConfig, + reviveConsumedRequestBodies?: boolean, ): RequestHandler { const fullConfig = typeof config === 'string' ? { target: config } : { ...config }; @@ -175,6 +179,10 @@ export function buildMiddleware( }); }; + if (reviveConsumedRequestBodies) { + fullConfig.onProxyReq = fixRequestBody; + } + return createProxyMiddleware(filter, fullConfig); } @@ -242,7 +250,13 @@ function configureMiddlewares( try { router.use( route, - buildMiddleware(pathPrefix, options.logger, route, proxyRouteConfig), + buildMiddleware( + pathPrefix, + options.logger, + route, + proxyRouteConfig, + options.reviveConsumedRequestBodies, + ), ); } catch (e) { if (options.skipInvalidProxies) { diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index cc7cf0000b..41a5df307f 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-rollbar-backend +## 0.1.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + +## 0.1.39 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + +## 0.1.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## 0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + +## 0.1.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.1.37 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index c570652d98..7a2f27cea0 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.37", + "version": "0.1.40-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -52,7 +52,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 0d4466b704..77235a2c74 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/plugin-rollbar +## 0.4.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.4.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.4.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.4.14 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index e2b49d62f2..f95f2cb8c4 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.4.14", + "version": "0.4.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -61,7 +61,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 44db8fc3ad..ed849b36f1 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.11.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## 0.2.17 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/plugin-scaffolder-backend@1.11.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/plugin-scaffolder-backend@1.11.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## 0.2.17-next.0 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + - @backstage/plugin-scaffolder-backend@1.11.0-next.0 + - @backstage/backend-common@0.18.2-next.0 + ## 0.2.15 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md index e55c6368c4..76f2ced66f 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/api-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -8,7 +8,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 { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; // @public diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 6a1ee7fd3b..de055be7e8 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.15", + "version": "0.2.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,6 +28,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "command-exists": "^1.2.9", "fs-extra": "10.1.0", @@ -40,7 +41,7 @@ "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "mock-fs": "^5.1.0", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" 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 15e5d25183..f8b049f95f 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 @@ -27,7 +27,7 @@ import os from 'os'; import { PassThrough } from 'stream'; import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'path'; -import type { ActionContext } from '@backstage/plugin-scaffolder-backend'; +import type { ActionContext } from '@backstage/plugin-scaffolder-node'; const executeShellCommand = jest.fn(); const commandExists = jest.fn(); diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index 7575627506..5c69b9dce2 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -27,10 +27,10 @@ import fs from 'fs-extra'; import path, { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; import { - createTemplateAction, fetchContents, executeShellCommand, } from '@backstage/plugin-scaffolder-backend'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; export class CookiecutterRunner { private readonly containerRunner: ContainerRunner; diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 491e3b04a4..65cc7fa77d 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.11.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## 0.4.10 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/plugin-scaffolder-backend@1.11.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## 0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/plugin-scaffolder-backend@1.11.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## 0.4.10-next.0 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + - @backstage/plugin-scaffolder-backend@1.11.0-next.0 + - @backstage/backend-common@0.18.2-next.0 + ## 0.4.8 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/api-report.md b/plugins/scaffolder-backend-module-rails/api-report.md index 551da91314..c91daa1344 100644 --- a/plugins/scaffolder-backend-module-rails/api-report.md +++ b/plugins/scaffolder-backend-module-rails/api-report.md @@ -6,7 +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 { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; // @public diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 408db6d559..2ae2a6dd3f 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.4.8", + "version": "0.4.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,6 +28,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "command-exists": "^1.2.9", "fs-extra": "^10.0.1" 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 4a5bd7dfd1..2a8a2554a2 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 @@ -19,10 +19,8 @@ import { JsonObject } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import fs from 'fs-extra'; -import { - createTemplateAction, - fetchContents, -} from '@backstage/plugin-scaffolder-backend'; +import { fetchContents } from '@backstage/plugin-scaffolder-backend'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { resolve as resolvePath } from 'path'; import { RailsNewRunner } from './railsNewRunner'; diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index ba1ca5cf7a..059b43bd90 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## 0.1.2 + +### Patch Changes + +- da418c89e4: Fix broken module exports and dependencies to match a backend module, rather than a frontend plugin. +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.2-next.1 + +### Patch Changes + +- da418c89e4: Fix broken module exports and dependencies to match a backend module, rather than a frontend plugin. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## 0.1.2-next.0 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/scaffolder-backend-module-sentry/api-report.md b/plugins/scaffolder-backend-module-sentry/api-report.md index 8b0c229e03..ef09bdebc0 100644 --- a/plugins/scaffolder-backend-module-sentry/api-report.md +++ b/plugins/scaffolder-backend-module-sentry/api-report.md @@ -4,7 +4,7 @@ ```ts import { Config } from '@backstage/config'; -import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public export function createSentryCreateProjectAction(options: { diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 8eaf5c398d..8c1e2f5742 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,16 +1,16 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.1.0", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { "access": "public", - "main": "dist/index.esm.js", + "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, "backstage": { - "role": "frontend-plugin" + "role": "backend-plugin-module" }, "scripts": { "start": "backstage-cli package start", @@ -24,23 +24,10 @@ "dependencies": { "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", - "@backstage/integration": "workspace:^", - "@backstage/plugin-scaffolder-backend": "workspace:^" - }, - "peerDependencies": { - "react": "^16.13.1 || ^17.0.0" + "@backstage/plugin-scaffolder-node": "workspace:^" }, "devDependencies": { - "@backstage/cli": "workspace:^", - "@backstage/core-app-api": "workspace:^", - "@backstage/dev-utils": "workspace:^", - "@backstage/test-utils": "workspace:^", - "@testing-library/jest-dom": "^5.10.1", - "@testing-library/react": "^12.1.3", - "@testing-library/user-event": "^14.0.0", - "@types/node": "*", - "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "@backstage/cli": "workspace:^" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts index bfcc8f016b..85a74eda07 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createTemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 64437c70a1..c099722329 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## 0.2.15 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## 0.2.15-next.0 + +### Patch Changes + +- d72866f0cc: Internal refactor to use the new `@backstage/plugin-scaffolder-node` package for some functionality +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + ## 0.2.13 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/api-report.md b/plugins/scaffolder-backend-module-yeoman/api-report.md index 0f8747e592..562fe52ac2 100644 --- a/plugins/scaffolder-backend-module-yeoman/api-report.md +++ b/plugins/scaffolder-backend-module-yeoman/api-report.md @@ -4,7 +4,7 @@ ```ts import { JsonObject } from '@backstage/types'; -import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public export function createRunYeomanAction(): TemplateAction<{ diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 2d4d7e8702..6b609d8b39 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.2.13", + "version": "0.2.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,7 +23,7 @@ }, "dependencies": { "@backstage/config": "workspace:^", - "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "winston": "^3.2.1", "yeoman-environment": "^3.9.1" diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts index ee5e8449f7..146c29f4ac 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts @@ -22,7 +22,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import os from 'os'; import { PassThrough } from 'stream'; import { createRunYeomanAction } from './yeoman'; -import type { ActionContext } from '@backstage/plugin-scaffolder-backend'; +import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { JsonObject } from '@backstage/types'; describe('run:yeoman', () => { diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts index f7b0c54b41..89ea10b6a6 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts @@ -15,7 +15,7 @@ */ import { JsonObject } from '@backstage/types'; -import { createTemplateAction } from '@backstage/plugin-scaffolder-backend'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { yeomanRun } from './yeomanRun'; /** diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 6da85db1be..2d31ac43c1 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,135 @@ # @backstage/plugin-scaffolder-backend +## 1.11.1-next.0 + +### Patch Changes + +- c6c78b4acb: throw error from catalog:fetch scaffolder action when entity is null and optional is false +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-backend@1.8.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + - @backstage/plugin-scaffolder-node@0.1.1-next.0 + +## 1.11.0 + +### Minor Changes + +- 0b2952ee4b: Added the option to overwrite files in the `targetPath` of the `template:fetch` action +- 127154930f: Renamed the export `scaffolderCatalogModule` to `catalogModuleTemplateKind` in order to follow the new recommended naming patterns of backend system items. This is technically a breaking change but in an alpha export, so take care to change your imports if you have already migrated to the new backend system. + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- ad3edc402d: **Deprecations**: The following are deprecated and should instead be imported from the new package `@backstage/plugin-scaffolder-node`: + + - `ActionContext` + - `createTemplateAction` + - `TaskSecrets` + - `TemplateAction` + +- 6c70919f1a: Provide better error messaging when GitHub fails due to missing team definitions +- 66cf22fdc4: Updated dependency `esbuild` to `^0.17.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2 + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/plugin-scaffolder-node@0.1.0 + - @backstage/catalog-model@1.2.0 + - @backstage/plugin-catalog-node@1.3.3 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-scaffolder-common@1.2.5 + +## 1.11.0-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## 1.11.0-next.1 + +### Minor Changes + +- 127154930f: Renamed the export `scaffolderCatalogModule` to `catalogModuleTemplateKind` in order to follow the new recommended naming patterns of backend system items. This is technically a breaking change but in an alpha export, so take care to change your imports if you have already migrated to the new backend system. + +### Patch Changes + +- 66cf22fdc4: Updated dependency `esbuild` to `^0.17.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.1 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/plugin-scaffolder-node@0.1.0-next.1 + +## 1.11.0-next.0 + +### Minor Changes + +- 0b2952ee4b: Added the option to overwrite files in the `targetPath` of the `template:fetch` action + +### Patch Changes + +- ad3edc402d: **Deprecations**: The following are deprecated and should instead be imported from the new package `@backstage/plugin-scaffolder-node`: + + - `ActionContext` + - `createTemplateAction` + - `TaskSecrets` + - `TemplateAction` + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-backend@1.7.2-next.0 + - @backstage/plugin-catalog-node@1.3.3-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 + ## 1.10.0 ### Minor Changes diff --git a/plugins/scaffolder-backend/alpha-api-report.md b/plugins/scaffolder-backend/alpha-api-report.md new file mode 100644 index 0000000000..3199444c6b --- /dev/null +++ b/plugins/scaffolder-backend/alpha-api-report.md @@ -0,0 +1,30 @@ +## API Report File for "@backstage/plugin-scaffolder-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { TaskBroker } from '@backstage/plugin-scaffolder-backend'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { TemplateFilter } from '@backstage/plugin-scaffolder-backend'; +import { TemplateGlobal } from '@backstage/plugin-scaffolder-backend'; + +// @alpha +export const catalogModuleTemplateKind: () => BackendFeature; + +// @alpha +export const scaffolderPlugin: ( + options: ScaffolderPluginOptions, +) => BackendFeature; + +// @alpha +export type ScaffolderPluginOptions = { + actions?: TemplateAction<any>[]; + taskWorkers?: number; + taskBroker?: TaskBroker; + additionalTemplateFilters?: Record<string, TemplateFilter>; + additionalTemplateGlobals?: Record<string, TemplateGlobal>; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 7f8ab0ad79..293ff7ed29 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -5,7 +5,7 @@ ```ts /// <reference types="node" /> -import { BackendFeature } from '@backstage/backend-plugin-api'; +import { ActionContext as ActionContext_2 } from '@backstage/plugin-scaffolder-node'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; @@ -24,38 +24,23 @@ import { Observable } from '@backstage/types'; import { Octokit } from 'octokit'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; -import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; +import { TaskSecrets as TaskSecrets_2 } from '@backstage/plugin-scaffolder-node'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; -import { UserEntity } from '@backstage/catalog-model'; import { Writable } from 'stream'; -// @public -export type ActionContext<Input extends JsonObject> = { - logger: Logger; - logStream: Writable; - secrets?: TaskSecrets; - workspacePath: string; - input: Input; - output(name: string, value: JsonValue): void; - createTemporaryDirectory(): Promise<string>; - templateInfo?: TemplateInfo; - isDryRun?: boolean; - user?: { - entity?: UserEntity; - ref?: string; - }; -}; +// @public @deprecated (undocumented) +export type ActionContext<TInput extends JsonObject> = ActionContext_2<TInput>; // @public export const createBuiltinActions: ( options: CreateBuiltInActionsOptions, -) => TemplateAction<JsonObject>[]; +) => TemplateAction_2<JsonObject>[]; // @public export interface CreateBuiltInActionsOptions { @@ -72,7 +57,7 @@ export interface CreateBuiltInActionsOptions { export function createCatalogRegisterAction(options: { catalogClient: CatalogApi; integrations: ScmIntegrations; -}): TemplateAction< +}): TemplateAction_2< | { catalogInfoUrl: string; optional?: boolean | undefined; @@ -85,13 +70,13 @@ export function createCatalogRegisterAction(options: { >; // @public -export function createCatalogWriteAction(): TemplateAction<{ +export function createCatalogWriteAction(): TemplateAction_2<{ filePath?: string | undefined; entity: Entity; }>; // @public -export function createDebugLogAction(): TemplateAction<{ +export function createDebugLogAction(): TemplateAction_2<{ message?: string | undefined; listWorkspace?: boolean | undefined; }>; @@ -99,7 +84,7 @@ export function createDebugLogAction(): TemplateAction<{ // @public export function createFetchCatalogEntityAction(options: { catalogClient: CatalogApi; -}): TemplateAction<{ +}): TemplateAction_2<{ entityRef: string; optional?: boolean | undefined; }>; @@ -108,7 +93,7 @@ export function createFetchCatalogEntityAction(options: { export function createFetchPlainAction(options: { reader: UrlReader; integrations: ScmIntegrations; -}): TemplateAction<{ +}): TemplateAction_2<{ url: string; targetPath?: string | undefined; }>; @@ -119,7 +104,7 @@ export function createFetchTemplateAction(options: { integrations: ScmIntegrations; additionalTemplateFilters?: Record<string, TemplateFilter>; additionalTemplateGlobals?: Record<string, TemplateGlobal>; -}): TemplateAction<{ +}): TemplateAction_2<{ url: string; targetPath?: string | undefined; values: any; @@ -131,12 +116,12 @@ export function createFetchTemplateAction(options: { }>; // @public -export const createFilesystemDeleteAction: () => TemplateAction<{ +export const createFilesystemDeleteAction: () => TemplateAction_2<{ files: string[]; }>; // @public -export const createFilesystemRenameAction: () => TemplateAction<{ +export const createFilesystemRenameAction: () => TemplateAction_2<{ files: Array<{ from: string; to: string; @@ -148,7 +133,7 @@ export const createFilesystemRenameAction: () => TemplateAction<{ export function createGithubActionsDispatchAction(options: { integrations: ScmIntegrations; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; workflowId: string; branchOrTagName: string; @@ -164,7 +149,7 @@ export function createGithubActionsDispatchAction(options: { export function createGithubIssuesLabelAction(options: { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; number: number; labels: string[]; @@ -194,7 +179,7 @@ export type CreateGithubPullRequestClientFactoryInput = { export function createGithubRepoCreateAction(options: { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; homepage?: string | undefined; @@ -261,7 +246,7 @@ export function createGithubRepoPushAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -300,7 +285,7 @@ export function createGithubWebhookAction(options: { integrations: ScmIntegrationRegistry; defaultWebhookSecret?: string; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; webhookUrl: string; webhookSecret?: string | undefined; @@ -315,7 +300,7 @@ export function createGithubWebhookAction(options: { export function createPublishAzureAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -330,7 +315,7 @@ export function createPublishAzureAction(options: { export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -347,7 +332,7 @@ export function createPublishBitbucketAction(options: { export function createPublishBitbucketCloudAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -360,7 +345,7 @@ export function createPublishBitbucketCloudAction(options: { export function createPublishBitbucketServerAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; defaultBranch?: string | undefined; @@ -377,7 +362,7 @@ export function createPublishBitbucketServerAction(options: { export function createPublishGerritAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description: string; defaultBranch?: string | undefined; @@ -391,7 +376,7 @@ export function createPublishGerritAction(options: { export function createPublishGerritReviewAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; branch?: string | undefined; sourcePath?: string | undefined; @@ -405,7 +390,7 @@ export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; description?: string | undefined; homepage?: string | undefined; @@ -478,7 +463,7 @@ export const createPublishGithubPullRequestAction: ({ integrations, githubCredentialsProvider, clientFactory, -}: CreateGithubPullRequestActionOptions) => TemplateAction<{ +}: CreateGithubPullRequestActionOptions) => TemplateAction_2<{ title: string; branchName: string; description: string; @@ -495,7 +480,7 @@ export const createPublishGithubPullRequestAction: ({ export function createPublishGitlabAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction<{ +}): TemplateAction_2<{ repoUrl: string; defaultBranch?: string | undefined; repoVisibility?: 'internal' | 'private' | 'public' | undefined; @@ -511,7 +496,7 @@ export function createPublishGitlabAction(options: { // @public export const createPublishGitlabMergeRequestAction: (options: { integrations: ScmIntegrationRegistry; -}) => TemplateAction<{ +}) => TemplateAction_2<{ repoUrl: string; title: string; description: string; @@ -528,10 +513,10 @@ export const createPublishGitlabMergeRequestAction: (options: { // @public export function createRouter(options: RouterOptions): Promise<express.Router>; -// @public +// @public @deprecated (undocumented) export const createTemplateAction: <TInput extends JsonObject>( - templateAction: TemplateAction<TInput>, -) => TemplateAction<TInput>; + templateAction: TemplateAction_2<TInput>, +) => TemplateAction_2<TInput>; // @public export type CreateWorkerOptions = { @@ -548,7 +533,7 @@ export type CreateWorkerOptions = { // @public export interface CurrentClaimedTask { createdBy?: string; - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; spec: TaskSpec; taskId: string; } @@ -629,7 +614,7 @@ export type OctokitWithPullRequestPluginClient = Octokit & { // @public export interface RouterOptions { // (undocumented) - actions?: TemplateAction<any>[]; + actions?: TemplateAction_2<any>[]; // (undocumented) additionalTemplateFilters?: Record<string, TemplateFilter>; // (undocumented) @@ -663,9 +648,6 @@ export type RunCommandOptions = { logStream?: Writable; }; -// @alpha -export const scaffolderCatalogModule: () => BackendFeature; - // @public (undocumented) export class ScaffolderEntitiesProcessor implements CatalogProcessor { // (undocumented) @@ -680,20 +662,6 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { validateEntityKind(entity: Entity): Promise<boolean>; } -// @alpha -export const scaffolderPlugin: ( - options: ScaffolderPluginOptions, -) => BackendFeature; - -// @alpha -export type ScaffolderPluginOptions = { - actions?: TemplateAction<any>[]; - taskWorkers?: number; - taskBroker?: TaskBroker; - additionalTemplateFilters?: Record<string, TemplateFilter>; - additionalTemplateGlobals?: Record<string, TemplateGlobal>; -}; - // @public export type SerializedTask = { id: string; @@ -702,7 +670,7 @@ export type SerializedTask = { createdAt: string; lastHeartbeatAt?: string; createdBy?: string; - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; }; // @public @@ -739,7 +707,7 @@ export interface TaskBroker { // @public export type TaskBrokerDispatchOptions = { spec: TaskSpec; - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; createdBy?: string; }; @@ -766,7 +734,7 @@ export interface TaskContext { // (undocumented) isDryRun?: boolean; // (undocumented) - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; // (undocumented) spec: TaskSpec; } @@ -793,15 +761,13 @@ export class TaskManager implements TaskContext { // (undocumented) getWorkspaceName(): Promise<string>; // (undocumented) - get secrets(): TaskSecrets | undefined; + get secrets(): TaskSecrets_2 | undefined; // (undocumented) get spec(): TaskSpecV1beta3; } -// @public -export type TaskSecrets = Record<string, string> & { - backstageToken?: string; -}; +// @public @deprecated (undocumented) +export type TaskSecrets = TaskSecrets_2; // @public export type TaskStatus = @@ -853,7 +819,7 @@ export interface TaskStore { export type TaskStoreCreateTaskOptions = { spec: TaskSpec; createdBy?: string; - secrets?: TaskSecrets; + secrets?: TaskSecrets_2; }; // @public @@ -890,30 +856,18 @@ export class TaskWorker { start(): void; } -// @public (undocumented) -export type TemplateAction<Input extends JsonObject> = { - id: string; - description?: string; - examples?: { - description: string; - example: string; - }[]; - supportsDryRun?: boolean; - schema?: { - input?: Schema; - output?: Schema; - }; - handler: (ctx: ActionContext<Input>) => Promise<void>; -}; +// @public @deprecated (undocumented) +export type TemplateAction<TInput extends JsonObject> = + TemplateAction_2<TInput>; // @public export class TemplateActionRegistry { // (undocumented) - get(actionId: string): TemplateAction<JsonObject>; + get(actionId: string): TemplateAction_2<JsonObject>; // (undocumented) - list(): TemplateAction<JsonObject>[]; + list(): TemplateAction_2<JsonObject>[]; // (undocumented) - register<TInput extends JsonObject>(action: TemplateAction<TInput>): void; + register<TInput extends JsonObject>(action: TemplateAction_2<TInput>): void; } // @public (undocumented) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 8a98ea4a3d..12ef31899c 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "1.10.0", + "version": "1.11.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -46,6 +58,7 @@ "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "@gitbeaker/core": "^35.6.0", "@gitbeaker/node": "^35.1.0", @@ -91,15 +104,14 @@ "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", "@types/zen-observable": "^0.8.0", - "esbuild": "^0.16.0", + "esbuild": "^0.17.0", "jest-when": "^3.1.0", "mock-fs": "^5.1.0", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3", "yaml": "^2.0.0" }, "files": [ - "alpha", "dist", "migrations", "config.d.ts", diff --git a/plugins/scaffolder-backend/scripts/build-nunjucks.js b/plugins/scaffolder-backend/scripts/build-nunjucks.js index 8b98ac4305..8183de77ee 100755 --- a/plugins/scaffolder-backend/scripts/build-nunjucks.js +++ b/plugins/scaffolder-backend/scripts/build-nunjucks.js @@ -16,7 +16,7 @@ */ /* eslint-disable no-restricted-syntax */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const path = require('path'); diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 71472abda6..182bb60223 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -13,20 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createBackendPlugin, coreServices, - createExtensionPoint, } from '@backstage/backend-plugin-api'; import { loggerToWinstonLogger } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; -import { catalogServiceRef } from '@backstage/plugin-catalog-node'; -import { TemplateFilter, TemplateGlobal } from './lib'; -import { createBuiltinActions, TaskBroker, TemplateAction } from './scaffolder'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { + scaffolderActionsExtensionPoint, + ScaffolderActionsExtensionPoint, + TemplateAction, +} from '@backstage/plugin-scaffolder-node'; +import { + TemplateFilter, + TemplateGlobal, + TaskBroker, +} from '@backstage/plugin-scaffolder-backend'; +import { createBuiltinActions } from './scaffolder'; import { createRouter } from './service/router'; /** * Catalog plugin options + * * @alpha */ export type ScaffolderPluginOptions = { @@ -37,44 +47,31 @@ export type ScaffolderPluginOptions = { additionalTemplateGlobals?: Record<string, TemplateGlobal>; }; -/** - * @alpha - * TODO: MOVE to scaffolder-node. - */ -interface ScaffolderActionsExtensionPoint { - addActions(...actions: TemplateAction<any>[]): void; -} - class ScaffolderActionsExtensionPointImpl implements ScaffolderActionsExtensionPoint { #actions = new Array<TemplateAction<any>>(); + addActions(...actions: TemplateAction<any>[]): void { this.#actions.push(...actions); } + get actions() { return this.#actions; } } -/** - * @alpha - * TODO: MOVE to scaffolder-node. - */ -export const scaffolderActionsExtensionPoint = - createExtensionPoint<ScaffolderActionsExtensionPoint>({ - id: 'scaffolder.actions', - }); - /** * Catalog plugin + * * @alpha */ export const scaffolderPlugin = createBackendPlugin( (options: ScaffolderPluginOptions) => ({ - id: 'scaffolder', + pluginId: 'scaffolder', register(env) { const actionsExtensions = new ScaffolderActionsExtensionPointImpl(); + env.registerExtensionPoint( scaffolderActionsExtensionPoint, actionsExtensions, diff --git a/plugins/scaffolder-backend/src/alpha.ts b/plugins/scaffolder-backend/src/alpha.ts new file mode 100644 index 0000000000..ac96f081ad --- /dev/null +++ b/plugins/scaffolder-backend/src/alpha.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 './modules'; +export { scaffolderPlugin } from './ScaffolderPlugin'; +export type { ScaffolderPluginOptions } from './ScaffolderPlugin'; diff --git a/plugins/scaffolder-backend/src/deprecated.ts b/plugins/scaffolder-backend/src/deprecated.ts new file mode 100644 index 0000000000..f8358b9071 --- /dev/null +++ b/plugins/scaffolder-backend/src/deprecated.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ActionContext as ActionContextNode, + createTemplateAction as createTemplateActionNode, + TaskSecrets as TaskSecretsNode, + TemplateAction as TemplateActionNode, +} from '@backstage/plugin-scaffolder-node'; +import { JsonObject } from '@backstage/types'; + +/** + * @public + * @deprecated Import from {@link @backstage/plugin-scaffolder-node#ActionContext} instead + */ +export type ActionContext<TInput extends JsonObject> = + ActionContextNode<TInput>; + +/** + * @public + * @deprecated Use `createTemplateAction` from `@backstage/plugin-scaffolder-node` instead + */ +export const createTemplateAction = createTemplateActionNode; + +/** + * @public + * @deprecated Use `TaskSecrets` from `@backstage/plugin-scaffolder-node` instead + */ +export type TaskSecrets = TaskSecretsNode; + +/** + * @public + * @deprecated Use `TemplateAction` from `@backstage/plugin-scaffolder-node` instead + */ +export type TemplateAction<TInput extends JsonObject> = + TemplateActionNode<TInput>; diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts index 985ada405e..ba2c25f80a 100644 --- a/plugins/scaffolder-backend/src/index.ts +++ b/plugins/scaffolder-backend/src/index.ts @@ -24,6 +24,5 @@ export * from './scaffolder'; export * from './service/router'; export * from './lib'; export * from './processor'; -export * from './extension'; -export { scaffolderPlugin } from './ScaffolderPlugin'; -export type { ScaffolderPluginOptions } from './ScaffolderPlugin'; + +export * from './deprecated'; diff --git a/plugins/scaffolder-backend/src/extension/ScaffolderCatalogModule.test.ts b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.test.ts similarity index 85% rename from plugins/scaffolder-backend/src/extension/ScaffolderCatalogModule.test.ts rename to plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.test.ts index 7d8c3cc140..7b8b99f5e2 100644 --- a/plugins/scaffolder-backend/src/extension/ScaffolderCatalogModule.test.ts +++ b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.test.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { ScaffolderEntitiesProcessor } from '../processor'; -import { scaffolderCatalogModule } from './ScaffolderCatalogModule'; +import { catalogModuleTemplateKind } from './catalogModuleTemplateKind'; import { startTestBackend } from '@backstage/backend-test-utils'; -describe('ScaffolderCatalogModule', () => { +describe('catalogModuleTemplateKind', () => { it('should register the extension point', async () => { const extensionPoint = { addProcessor: jest.fn() }; await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], - features: [scaffolderCatalogModule()], + features: [catalogModuleTemplateKind()], }); expect(extensionPoint.addProcessor).toHaveBeenCalledWith( diff --git a/plugins/scaffolder-backend/src/extension/ScaffolderCatalogModule.ts b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts similarity index 84% rename from plugins/scaffolder-backend/src/extension/ScaffolderCatalogModule.ts rename to plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts index 4e9b6f75c9..3359bd1dba 100644 --- a/plugins/scaffolder-backend/src/extension/ScaffolderCatalogModule.ts +++ b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts @@ -13,17 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createBackendModule } from '@backstage/backend-plugin-api'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { ScaffolderEntitiesProcessor } from '../processor'; /** + * Registers support for the Template kind to the catalog backend plugin. + * * @alpha - * Registers the ScaffolderEntitiesProcessor with the catalog processing extension point. */ -export const scaffolderCatalogModule = createBackendModule({ +export const catalogModuleTemplateKind = createBackendModule({ moduleId: 'scaffolder', - pluginId: 'catalog', + pluginId: 'templateKind', register(env) { env.registerInit({ deps: { diff --git a/plugins/scaffolder-backend/src/modules/index.ts b/plugins/scaffolder-backend/src/modules/index.ts new file mode 100644 index 0000000000..993a66815a --- /dev/null +++ b/plugins/scaffolder-backend/src/modules/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 { catalogModuleTemplateKind } from './catalogModuleTemplateKind'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts index 8a2a00d121..d044d095cd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts @@ -16,7 +16,7 @@ import { JsonObject } from '@backstage/types'; import { ConflictError, NotFoundError } from '@backstage/errors'; -import { TemplateAction } from './types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; /** * Registry of all registered template actions. diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts index f8bdf2bdcb..52c76c8769 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts @@ -90,7 +90,7 @@ describe('catalog:fetch', () => { expect(mockContext.output).toHaveBeenCalledWith('entity', null); }); - it('should throw error if entity not in catalog and optional is false', async () => { + it('should throw error if entity fetch fails from catalog and optional is false', async () => { getEntityByRef.mockImplementationOnce(() => { throw new Error('Not found'); }); @@ -109,4 +109,22 @@ describe('catalog:fetch', () => { }); expect(mockContext.output).not.toHaveBeenCalled(); }); + + it('should throw error if entity not in catalog and optional is false', async () => { + getEntityByRef.mockReturnValueOnce(null); + + await expect( + action.handler({ + ...mockContext, + input: { + entityRef: 'component:default/test', + }, + }), + ).rejects.toThrow('Entity component:default/test not found'); + + expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { + token: 'secret', + }); + expect(mockContext.output).not.toHaveBeenCalled(); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts index c45eb62fce..17e510ce64 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts @@ -15,7 +15,7 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; const id = 'catalog:fetch'; @@ -93,6 +93,11 @@ export function createFetchCatalogEntityAction(options: { throw e; } } + + if (!entity && !optional) { + throw new Error(`Entity ${entityRef} not found`); + } + ctx.output('entity', entity ?? null); }, }); 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 48fec3db03..e866eb12ba 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -18,7 +18,7 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; import { stringifyEntityRef } from '@backstage/catalog-model'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; const id = 'catalog:register'; 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 5257839f2d..5c5a62956f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -15,7 +15,7 @@ */ import fs from 'fs-extra'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import * as yaml from 'yaml'; import { Entity } from '@backstage/catalog-model'; import { resolveSafeChildPath } from '@backstage/backend-common'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 7e954295a1..d83d9c5b07 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -23,6 +23,7 @@ import { ScmIntegrations, } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { createCatalogRegisterAction, createCatalogWriteAction, @@ -30,7 +31,6 @@ import { } from './catalog'; import { TemplateFilter, TemplateGlobal } from '../../../lib'; -import { TemplateAction } from '../types'; import { createDebugLogAction } from './debug'; import { createFetchPlainAction, createFetchTemplateAction } from './fetch'; import { 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 3863623563..481eb9dfdd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -16,7 +16,7 @@ import { readdir, stat } from 'fs-extra'; import { relative, join } from 'path'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; const id = 'debug:log'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts index 0469ffec06..ed37a39a98 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + jest.mock('./helpers'); import os from 'os'; 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 b3b984f544..c10f696bb9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts @@ -17,7 +17,7 @@ import { UrlReader, resolveSafeChildPath } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from './helpers'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; /** * Downloads content and places it in the workspace, or optionally 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 6658e1915d..15548ffea7 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 @@ -26,8 +26,11 @@ import { import { ScmIntegrations } from '@backstage/integration'; import { PassThrough } from 'stream'; import { fetchContents } from './helpers'; -import { ActionContext, TemplateAction } from '../../types'; import { createFetchTemplateAction } from './template'; +import { + ActionContext, + TemplateAction, +} from '@backstage/plugin-scaffolder-node'; jest.mock('./helpers', () => ({ fetchContents: jest.fn(), 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 81b3f4577c..68c205ce59 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -19,7 +19,7 @@ import { resolveSafeChildPath, UrlReader } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from './helpers'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import globby from 'globby'; import fs from 'fs-extra'; import { isBinaryFile } from 'isbinaryfile'; 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 757e9c682e..2f955c16ff 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createTemplateAction } from '../../createTemplateAction'; + +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { InputError } from '@backstage/errors'; import { resolveSafeChildPath } from '@backstage/backend-common'; import fs from 'fs-extra'; 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 654ee7b150..c29ba221c4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createTemplateAction } from '../../createTemplateAction'; -import { resolveSafeChildPath } from '@backstage/backend-common'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { resolveSafeChildPath } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import fs from 'fs-extra'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts index 0782ea40cd..3d59e43695 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts @@ -14,9 +14,6 @@ * limitations under the License. */ -import { TemplateAction } from '../../types'; -import { createGithubActionsDispatchAction } from './githubActionsDispatch'; - import { ScmIntegrations, DefaultGithubCredentialsProvider, @@ -24,7 +21,9 @@ import { } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { PassThrough } from 'stream'; +import { createGithubActionsDispatchAction } from './githubActionsDispatch'; const mockOctokit = { rest: { 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 b9d77559e0..cd1593ee9f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Octokit } from 'octokit'; -import { createTemplateAction } from '../../createTemplateAction'; import { parseRepoUrl } from '../publish/util'; import { getOctokitOptions } from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts index bc9b797000..a1a0274304 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts @@ -22,8 +22,8 @@ import { } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { PassThrough } from 'stream'; -import { TemplateAction } from '../../types'; const mockOctokit = { rest: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts index 9c849edb6f..2dad0aa33a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { getOctokitOptions } from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts index 824db27e84..a9ef8c3daa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TemplateAction } from '../../types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; jest.mock('../helpers'); @@ -42,6 +42,7 @@ const mockOctokit = { }, teams: { addOrUpdateRepoPermissionsInOrg: jest.fn(), + getByName: jest.fn(), }, }, }; @@ -96,6 +97,13 @@ describe('github:repo:create', () => { data: { type: 'Organization' }, }); + mockOctokit.rest.teams.getByName.mockResolvedValue({ + data: { + name: 'blam', + id: 42, + }, + }); + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); await action.handler(mockContext); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts index 835fc79238..b5f779b8a7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; import { Octokit } from 'octokit'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { parseRepoUrl } from '../publish/util'; import { createGithubRepoWithCollaboratorsAndTopics, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts index 24777163d4..517347c733 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TemplateAction } from '../../types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; jest.mock('../helpers'); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts index 5a5ffc015c..6e3892cee5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { @@ -20,7 +21,7 @@ import { ScmIntegrationRegistry, } from '@backstage/integration'; import { Octokit } from 'octokit'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { parseRepoUrl } from '../publish/util'; import { getOctokitOptions, initRepoPushAndProtect } from './helpers'; import * as inputProps from './inputProperties'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts index 97fdb3d634..7901210a6e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts @@ -22,8 +22,8 @@ import { } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { PassThrough } from 'stream'; -import { TemplateAction } from '../../types'; const mockOctokit = { rest: { 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 4547ce4492..4d487befc7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { emitterEventNames } from '@octokit/webhooks'; import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts index dae5e16374..75c8f25d8b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { assertError, InputError } from '@backstage/errors'; +import { assertError, InputError, NotFoundError } from '@backstage/errors'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, @@ -135,6 +135,10 @@ export async function createGithubRepoWithCollaboratorsAndTopics( username: owner, }); + if (access?.startsWith(`${owner}/`)) { + await validateAccessTeam(client, access); + } + const repoCreationPromise = user.data.type === 'Organization' ? client.rest.repos.createInOrg({ @@ -351,3 +355,22 @@ function extractCollaboratorName( if ('user' in collaborator) return collaborator.user; return collaborator.team; } + +async function validateAccessTeam(client: Octokit, access: string) { + const [org, team_slug] = access.split('/'); + try { + // Below rule disabled because of a 'getByName' check for a different library + // incorrectly triggers here. + // eslint-disable-next-line testing-library/no-await-sync-query + await client.rest.teams.getByName({ + org, + team_slug, + }); + } catch (e) { + if (e.response.data.message === 'Not Found') { + const message = `Received 'Not Found' from the API; one of org: + ${org} or team: ${team_slug} was not found within GitHub.`; + throw new NotFoundError(message); + } + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index ca5e40bdb0..2d2775572f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + jest.mock('azure-devops-node-api', () => ({ WebApi: jest.fn(), getPersonalAccessTokenHandler: jest.fn().mockReturnValue(() => {}), 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 60b13b717d..083c18c94f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -20,7 +20,7 @@ import { initRepoAndPush } from '../helpers'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Config } from '@backstage/config'; /** 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 8f72157fc2..74e5e8c1ab 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -19,9 +19,9 @@ import { BitbucketIntegrationConfig, ScmIntegrationRegistry, } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; -import { createTemplateAction } from '../../createTemplateAction'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts index 02e29c33de..2c17bf2dbb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts @@ -16,9 +16,9 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; -import { createTemplateAction } from '../../createTemplateAction'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts index 8c96c14463..f2f581eadb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts @@ -19,9 +19,9 @@ import { getBitbucketServerRequestOptions, ScmIntegrationRegistry, } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; -import { createTemplateAction } from '../../createTemplateAction'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts index 83653b3052..b5bcc31c62 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import crypto from 'crypto'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; @@ -21,7 +22,7 @@ import { getGerritRequestOptions, ScmIntegrationRegistry, } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts index 17068654c0..73e74f5cbc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import crypto from 'crypto'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { commitAndPushRepo } from '../helpers'; 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 c2681ed527..94b51f3e94 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 @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TemplateAction } from '../../types'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; jest.mock('../helpers'); @@ -45,6 +45,7 @@ const mockOctokit = { replaceAllTopics: jest.fn(), }, teams: { + getByName: jest.fn(), addOrUpdateRepoPermissionsInOrg: jest.fn(), }, }, @@ -96,11 +97,41 @@ describe('publish:github', () => { }); }); + it('should fail to create if the team is not found in the org', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.teams.getByName.mockRejectedValue({ + response: { + status: 404, + data: { + message: 'Not Found', + documentation_url: + 'https://docs.github.com/en/rest/teams/teams#add-or-update-team-repository-permissions', + }, + }, + }); + + await expect(action.handler(mockContext)).rejects.toThrow( + "Received 'Not Found' from the API;", + ); + + expect(mockOctokit.rest.repos.createInOrg).not.toHaveBeenCalled(); + }); + it('should call the githubApis with the correct values for createInOrg', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'Organization' }, }); + mockOctokit.rest.teams.getByName.mockResolvedValue({ + data: { + name: 'blam', + id: 42, + }, + }); + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); await action.handler(mockContext); @@ -518,6 +549,30 @@ describe('publish:github', () => { }); }); + it('should provide an adequate failure message when adding access', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.teams.getByName.mockRejectedValue({ + response: { + status: 404, + data: { + message: 'Not Found', + documentation_url: + 'https://docs.github.com/en/rest/teams/teams#add-or-update-team-repository-permissions', + }, + }, + }); + await expect(action.handler(mockContext)).rejects.toThrow( + "Received 'Not Found' from the API;", + ); + + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).not.toHaveBeenCalled(); + }); + it('should add outside collaborators when provided', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, 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 add6fcc79c..185239c73f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { @@ -20,7 +21,7 @@ import { ScmIntegrationRegistry, } from '@backstage/integration'; import { Octokit } from 'octokit'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { createGithubRepoWithCollaboratorsAndTopics, getOctokitOptions, 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 e6649d1ffe..70fff20446 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 @@ -20,11 +20,14 @@ import { GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; +import { + ActionContext, + TemplateAction, +} from '@backstage/plugin-scaffolder-node'; import mockFs from 'mock-fs'; import os from 'os'; import { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; -import { ActionContext, TemplateAction } from '../../types'; import { createPublishGithubPullRequestAction, OctokitWithPullRequestPluginClient, 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 d0ee979be3..764354e8ec 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -20,11 +20,11 @@ import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; -import { createTemplateAction } from '../../createTemplateAction'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Octokit } from 'octokit'; import { InputError, CustomErrorBase } from '@backstage/errors'; -import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { resolveSafeChildPath } from '@backstage/backend-common'; +import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { getOctokitOptions } from '../github/helpers'; import { SerializedFile, 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 dc1d26d416..a9c4522dd3 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 @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + jest.mock('../helpers'); import { createPublishGitlabAction } from './gitlab'; 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 53669efd0b..2e7520b77f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -16,10 +16,10 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; -import { createTemplateAction } from '../../createTemplateAction'; import { Config } from '@backstage/config'; /** diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts index d1888fd9d2..c3d0fe58f1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts @@ -16,11 +16,11 @@ import { getRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import mockFs from 'mock-fs'; import os from 'os'; import { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; -import { TemplateAction } from '../../types'; import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; 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 2a71c9f748..bb27410402 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createTemplateAction } from '../../createTemplateAction'; + +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Gitlab } from '@gitbeaker/node'; import { Types } from '@gitbeaker/core'; import path from 'path'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/index.ts index d5f77b7b6f..9ea0607f06 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/index.ts @@ -16,5 +16,3 @@ export * from './builtin'; export { TemplateActionRegistry } from './TemplateActionRegistry'; -export { createTemplateAction } from './createTemplateAction'; -export type { ActionContext, TemplateAction } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts index 6f11d3b6e8..c02375554c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts @@ -14,8 +14,9 @@ * limitations under the License. */ +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { JsonObject } from '@backstage/types'; -import { TemplateAction, TemplateActionRegistry } from '../actions'; +import { TemplateActionRegistry } from '../actions'; /** @internal */ export class DecoratedActionsRegistry extends TemplateActionRegistry { diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts index 17f9092bff..1929e3b8ac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts @@ -26,12 +26,15 @@ import { serializeDirectoryContents, } from '../../lib/files'; import { TemplateFilter, TemplateGlobal } from '../../lib/templating'; -import { createTemplateAction, TemplateActionRegistry } from '../actions'; +import { TemplateActionRegistry } from '../actions'; import { NunjucksWorkflowRunner } from '../tasks/NunjucksWorkflowRunner'; -import { TaskSecrets } from '../tasks/types'; import { DecoratedActionsRegistry } from './DecoratedActionsRegistry'; import fs from 'fs-extra'; import { resolveSafeChildPath } from '@backstage/backend-common'; +import { + createTemplateAction, + TaskSecrets, +} from '@backstage/plugin-scaffolder-node'; interface DryRunInput { spec: TaskSpec; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 24a35078e2..453cfae83b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -22,8 +22,9 @@ import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { TaskContext, TaskSecrets } from './types'; +import { TaskContext } from './types'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; // The Stream module is lazy loaded, so make sure it's in the module cache before mocking fs diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 5893a88485..516abe79e4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -26,7 +26,7 @@ import { PassThrough } from 'stream'; import { generateExampleOutput, isTruthy } from './helper'; import { validate as validateJsonSchema } from 'jsonschema'; import { parseRepoUrl } from '../actions/builtin/publish/util'; -import { TemplateAction, TemplateActionRegistry } from '../actions'; +import { TemplateActionRegistry } from '../actions'; import { TemplateFilter, SecureTemplater, @@ -38,6 +38,7 @@ import { TaskSpecV1beta3, TaskStep, } from '@backstage/plugin-scaffolder-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; import { createCounterMetric, createHistogramMetric } from '../../util/metrics'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 5eeb73711e..2255798d5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -17,9 +17,10 @@ import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker, TaskManager } from './StorageTaskBroker'; -import { TaskSecrets, SerializedTaskEvent } from './types'; +import { SerializedTaskEvent } from './types'; async function createStore(): Promise<DatabaseTaskStore> { const manager = DatabaseManager.fromConfig( diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 7a015c111f..d01351d8ae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -15,6 +15,7 @@ */ import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { JsonObject, Observable } from '@backstage/types'; import { Logger } from 'winston'; import ObservableImpl from 'zen-observable'; @@ -25,7 +26,6 @@ import { TaskBrokerDispatchOptions, TaskCompletionState, TaskContext, - TaskSecrets, TaskStore, } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index 2af7d6a340..f1c1a7bdac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { DatabaseTaskStore } from './DatabaseTaskStore'; export type { DatabaseTaskStoreOptions } from './DatabaseTaskStore'; export { TaskManager } from './StorageTaskBroker'; @@ -20,7 +21,6 @@ export type { CurrentClaimedTask } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; export type { CreateWorkerOptions } from './TaskWorker'; export type { - TaskSecrets, TaskCompletionState, TaskStoreEmitOptions, TaskStoreListEventsOptions, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 5d3f8113ee..89f6c3e213 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -16,6 +16,7 @@ import { JsonValue, JsonObject, Observable } from '@backstage/types'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; /** * The status of each step of the Task @@ -71,15 +72,6 @@ export type SerializedTaskEvent = { createdAt: string; }; -/** - * TaskSecrets - * - * @public - */ -export type TaskSecrets = Record<string, string> & { - backstageToken?: string; -}; - /** * The result of {@link TaskBroker.dispatch} * diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9ff8d43625..c339de8d10 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -43,7 +43,6 @@ import { DatabaseTaskStore, TaskBroker, TaskWorker, - TemplateAction, TemplateActionRegistry, } from '../scaffolder'; import { createDryRunner } from '../scaffolder/dryrun'; @@ -53,6 +52,7 @@ import { IdentityApi, IdentityApiGetIdentityRequest, } from '@backstage/plugin-auth-node'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; /** * RouterOptions diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index a74ea5b13e..c9c785d777 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-scaffolder-common +## 1.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/types@1.0.2 + +## 1.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/types@1.0.2 + +## 1.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/types@1.0.2 + +## 1.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + ## 1.2.4 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index e1ba605c30..4e78d531e3 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "1.2.4", + "version": "1.2.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-node/.eslintrc.js b/plugins/scaffolder-node/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/scaffolder-node/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md new file mode 100644 index 0000000000..5bc9d498b6 --- /dev/null +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -0,0 +1,58 @@ +# @backstage/plugin-scaffolder-node + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + +## 0.1.0 + +### Minor Changes + +- d72866f0cc: New package that takes over some of the types and functionality from `@backstage/plugin-scaffolder-backend` that are shared with other modules + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/catalog-model@1.2.0 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5 + +## 0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## 0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.3.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + +## 0.1.0-next.0 + +### Minor Changes + +- d72866f0cc: New package that takes over some of the types and functionality from `@backstage/plugin-scaffolder-backend` that are shared with other modules + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/backend-plugin-api@0.3.2-next.0 diff --git a/plugins/scaffolder-node/README.md b/plugins/scaffolder-node/README.md new file mode 100644 index 0000000000..8b52daa785 --- /dev/null +++ b/plugins/scaffolder-node/README.md @@ -0,0 +1,3 @@ +# plugin-scaffolder-node + +Houses types and utilities for building scaffolder-related modules. diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md new file mode 100644 index 0000000000..506ea0517d --- /dev/null +++ b/plugins/scaffolder-node/api-report.md @@ -0,0 +1,68 @@ +## API Report File for "@backstage/plugin-scaffolder-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="node" /> + +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; +import { Logger } from 'winston'; +import { Schema } from 'jsonschema'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import { UserEntity } from '@backstage/catalog-model'; +import { Writable } from 'stream'; + +// @public +export type ActionContext<TInput extends JsonObject> = { + logger: Logger; + logStream: Writable; + secrets?: TaskSecrets; + workspacePath: string; + input: TInput; + output(name: string, value: JsonValue): void; + createTemporaryDirectory(): Promise<string>; + templateInfo?: TemplateInfo; + isDryRun?: boolean; + user?: { + entity?: UserEntity; + ref?: string; + }; +}; + +// @public +export const createTemplateAction: <TInput extends JsonObject>( + templateAction: TemplateAction<TInput>, +) => TemplateAction<TInput>; + +// @alpha +export interface ScaffolderActionsExtensionPoint { + // (undocumented) + addActions(...actions: TemplateAction<any>[]): void; +} + +// @alpha +export const scaffolderActionsExtensionPoint: ExtensionPoint<ScaffolderActionsExtensionPoint>; + +// @public +export type TaskSecrets = Record<string, string> & { + backstageToken?: string; +}; + +// @public (undocumented) +export type TemplateAction<TInput extends JsonObject> = { + id: string; + description?: string; + examples?: { + description: string; + example: string; + }[]; + supportsDryRun?: boolean; + schema?: { + input?: Schema; + output?: Schema; + }; + handler: (ctx: ActionContext<TInput>) => Promise<void>; +}; +``` diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json new file mode 100644 index 0000000000..642a59b801 --- /dev/null +++ b/plugins/scaffolder-node/package.json @@ -0,0 +1,41 @@ +{ + "name": "@backstage/plugin-scaffolder-node", + "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", + "version": "0.1.1-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-scaffolder-common": "workspace:^", + "@backstage/types": "workspace:^", + "jsonschema": "^1.2.6", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "alpha", + "dist" + ] +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts similarity index 99% rename from plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts rename to plugins/scaffolder-node/src/actions/createTemplateAction.ts index 8893fa0f20..24d88a1681 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -19,6 +19,7 @@ import { TemplateAction } from './types'; /** * This function is used to create new template actions to get type safety. + * * @public */ export const createTemplateAction = <TInput extends JsonObject>( diff --git a/plugins/scaffolder-node/src/actions/index.ts b/plugins/scaffolder-node/src/actions/index.ts new file mode 100644 index 0000000000..7fdee6d692 --- /dev/null +++ b/plugins/scaffolder-node/src/actions/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createTemplateAction } from './createTemplateAction'; +export { type ActionContext, type TemplateAction } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts similarity index 87% rename from plugins/scaffolder-backend/src/scaffolder/actions/types.ts rename to plugins/scaffolder-node/src/actions/types.ts index 0912cc038b..5a4f5796a1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2023 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import { Logger } from 'winston'; import { Writable } from 'stream'; import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; -import { TaskSecrets } from '../tasks'; +import { TaskSecrets } from '../tasks/types'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { UserEntity } from '@backstage/catalog-model'; @@ -26,12 +26,12 @@ import { UserEntity } from '@backstage/catalog-model'; * ActionContext is passed into scaffolder actions. * @public */ -export type ActionContext<Input extends JsonObject> = { +export type ActionContext<TInput extends JsonObject> = { logger: Logger; logStream: Writable; secrets?: TaskSecrets; workspacePath: string; - input: Input; + input: TInput; output(name: string, value: JsonValue): void; /** @@ -63,7 +63,7 @@ export type ActionContext<Input extends JsonObject> = { }; /** @public */ -export type TemplateAction<Input extends JsonObject> = { +export type TemplateAction<TInput extends JsonObject> = { id: string; description?: string; examples?: { description: string; example: string }[]; @@ -72,5 +72,5 @@ export type TemplateAction<Input extends JsonObject> = { input?: Schema; output?: Schema; }; - handler: (ctx: ActionContext<Input>) => Promise<void>; + handler: (ctx: ActionContext<TInput>) => Promise<void>; }; diff --git a/plugins/scaffolder/src/layouts/types.ts b/plugins/scaffolder-node/src/extensions.ts similarity index 56% rename from plugins/scaffolder/src/layouts/types.ts rename to plugins/scaffolder-node/src/extensions.ts index 94186d82bf..a706101043 100644 --- a/plugins/scaffolder/src/layouts/types.ts +++ b/plugins/scaffolder-node/src/extensions.ts @@ -13,21 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { FormProps } from '@rjsf/core'; + +import { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { TemplateAction } from './actions'; /** - * The field template from \@rjsf/core which is a react component that gets passed \@rjsf/core field related props. + * Extension point for managing scaffolder actions. * - * @public + * @alpha */ -export type LayoutTemplate<T = any> = FormProps<T>['ObjectFieldTemplate']; - -/** - * The type of layouts that is passed to the TemplateForms - * - * @public - */ -export interface LayoutOptions<P = any> { - name: string; - component: LayoutTemplate<P>; +export interface ScaffolderActionsExtensionPoint { + addActions(...actions: TemplateAction<any>[]): void; } + +/** + * Extension point for managing scaffolder actions. + * + * @alpha + */ +export const scaffolderActionsExtensionPoint = + createExtensionPoint<ScaffolderActionsExtensionPoint>({ + id: 'scaffolder.actions', + }); diff --git a/plugins/scaffolder-node/src/index.ts b/plugins/scaffolder-node/src/index.ts new file mode 100644 index 0000000000..2a7c06234e --- /dev/null +++ b/plugins/scaffolder-node/src/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The scaffolder-node module for `@backstage/plugin-scaffolder-backend`. + * + * @packageDocumentation + */ + +export * from './actions'; +export * from './tasks'; +export { + scaffolderActionsExtensionPoint, + type ScaffolderActionsExtensionPoint, +} from './extensions'; diff --git a/plugins/scaffolder-node/src/setupTests.ts b/plugins/scaffolder-node/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/scaffolder-node/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts new file mode 100644 index 0000000000..0c1d0d813e --- /dev/null +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { type TaskSecrets } from './types'; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts new file mode 100644 index 0000000000..c17b3cbde3 --- /dev/null +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * TaskSecrets + * + * @public + */ +export type TaskSecrets = Record<string, string> & { + backstageToken?: string; +}; diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 013cd393a3..24ccc34e4e 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,110 @@ # @backstage/plugin-scaffolder-react +## 1.1.1-next.0 + +### Patch Changes + +- c8d78b9ae9: fix bug with `hasErrors` returning false when dealing with empty objects +- 928a12a9b3: Internal refactor of `/alpha` exports. +- cc418d652a: scaffolder/next: Added the ability to get the fields definition in the schema in the validation function +- d4100d0ec4: Fix alignment bug for owners on `TemplateCard` +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + +## 1.1.0 + +### Minor Changes + +- a07750745b: Added `DescriptionField` field override to the `next/scaffolder` +- a521379688: Migrating the `TemplateEditorPage` to work with the new components from `@backstage/plugin-scaffolder-react` +- 8c2966536b: Embed scaffolder workflow in other components +- 5555e17313: refactor `createAsyncValidators` to be recursive to ensure validators are called in nested schemas. + +### Patch Changes + +- 04f717a8e1: `scaffolder/next`: bump `react-jsonschema-form` libraries to `v5-stable` +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- cbab8ac107: lock versions of `@rjsf/*-beta` packages +- 346d6b6630: Upgrade `@rjsf` version 5 dependencies to `beta.18` +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- d2ddde2108: Add `ScaffolderLayouts` to `NextScaffolderPage` +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-scaffolder-common@1.2.5 + +## 1.1.0-next.2 + +### Minor Changes + +- 5555e17313: refactor `createAsyncValidators` to be recursive to ensure validators are called in nested schemas. + +### Patch Changes + +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## 1.1.0-next.1 + +### Patch Changes + +- 04f717a8e1: `scaffolder/next`: bump `react-jsonschema-form` libraries to `v5-stable` +- 346d6b6630: Upgrade `@rjsf` version 5 dependencies to `beta.18` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + +## 1.1.0-next.0 + +### Minor Changes + +- 8c2966536b: Embed scaffolder workflow in other components + +### Patch Changes + +- cbab8ac107: lock versions of `@rjsf/*-beta` packages +- d2ddde2108: Add `ScaffolderLayouts` to `NextScaffolderPage` +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + ## 1.0.0 ### Major Changes diff --git a/plugins/scaffolder-react/alpha-api-report.md b/plugins/scaffolder-react/alpha-api-report.md new file mode 100644 index 0000000000..ad44cd3707 --- /dev/null +++ b/plugins/scaffolder-react/alpha-api-report.md @@ -0,0 +1,222 @@ +## API Report File for "@backstage/plugin-scaffolder-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="react" /> + +import { ApiHolder } from '@backstage/core-plugin-api'; +import { ComponentType } from 'react'; +import { CustomFieldExtensionSchema } from '@backstage/plugin-scaffolder-react'; +import { Dispatch } from 'react'; +import { Extension } from '@backstage/core-plugin-api'; +import { FieldExtensionComponent } from '@backstage/plugin-scaffolder-react'; +import { FieldProps } from '@rjsf/utils'; +import { FieldValidation } from '@rjsf/utils'; +import { FormProps as FormProps_2 } from '@rjsf/core-v5'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; +import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; +import { PropsWithChildren } from 'react'; +import { default as React_2 } from 'react'; +import { ReactNode } from 'react'; +import { RJSFSchema } from '@rjsf/utils'; +import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; +import { SetStateAction } from 'react'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; +import { UIOptionsType } from '@rjsf/utils'; +import { UiSchema } from '@rjsf/utils'; + +// @alpha +export const createFieldValidation: () => FieldValidation; + +// @alpha +export function createNextScaffolderFieldExtension< + TReturnValue = unknown, + TInputProps extends UIOptionsType = {}, +>( + options: NextFieldExtensionOptions<TReturnValue, TInputProps>, +): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>; + +// @alpha +export const DefaultTemplateOutputs: (props: { + output?: ScaffolderTaskOutput; +}) => JSX.Element | null; + +// @alpha (undocumented) +export const EmbeddableWorkflow: (props: WorkflowProps) => JSX.Element; + +// @alpha +export const extractSchemaFromStep: (inputStep: JsonObject) => { + uiSchema: UiSchema; + schema: JsonObject; +}; + +// @alpha (undocumented) +export const Form: ComponentType<FormProps_2<any, RJSFSchema, any>>; + +// @alpha +export type FormProps = Pick< + FormProps_2, + 'transformErrors' | 'noHtml5Validate' +>; + +// @alpha +export type NextCustomFieldValidator<TFieldReturnValue> = ( + data: TFieldReturnValue, + field: FieldValidation, + context: { + apiHolder: ApiHolder; + formData: JsonObject; + schema: JsonObject; + }, +) => void | Promise<void>; + +// @alpha +export interface NextFieldExtensionComponentProps< + TFieldReturnValue, + TUiOptions = {}, +> extends PropsWithChildren<FieldProps<TFieldReturnValue>> { + // (undocumented) + uiSchema?: UiSchema<TFieldReturnValue> & { + 'ui:options'?: TUiOptions & UIOptionsType; + }; +} + +// @alpha +export type NextFieldExtensionOptions< + TFieldReturnValue = unknown, + TInputProps = unknown, +> = { + name: string; + component: ( + props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>, + ) => JSX.Element | null; + validation?: NextCustomFieldValidator<TFieldReturnValue>; + schema?: CustomFieldExtensionSchema; +}; + +// @alpha +export interface ParsedTemplateSchema { + // (undocumented) + description?: string; + // (undocumented) + mergedSchema: JsonObject; + // (undocumented) + schema: JsonObject; + // (undocumented) + title: string; + // (undocumented) + uiSchema: UiSchema; +} + +// @alpha +export const ReviewState: (props: ReviewStateProps) => JSX.Element; + +// @alpha +export type ReviewStateProps = { + schemas: ParsedTemplateSchema[]; + formState: JsonObject; +}; + +// @alpha +export const Stepper: (stepperProps: StepperProps) => JSX.Element; + +// @alpha +export type StepperProps = { + manifest: TemplateParameterSchema; + extensions: NextFieldExtensionOptions<any, any>[]; + templateName?: string; + FormProps?: FormProps; + initialState?: Record<string, JsonValue>; + onCreate: (values: Record<string, JsonValue>) => Promise<void>; + components?: { + ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element; + createButtonText?: ReactNode; + reviewButtonText?: ReactNode; + }; + layouts?: LayoutOptions[]; +}; + +// @alpha +export const TemplateCard: (props: TemplateCardProps) => JSX.Element; + +// @alpha +export interface TemplateCardProps { + // (undocumented) + additionalLinks?: { + icon: IconComponent; + text: string; + url: string; + }[]; + // (undocumented) + onSelected?: (template: TemplateEntityV1beta3) => void; + // (undocumented) + template: TemplateEntityV1beta3; +} + +// @alpha +export const TemplateGroup: (props: TemplateGroupProps) => JSX.Element; + +// @alpha +export interface TemplateGroupProps { + // (undocumented) + components?: { + CardComponent?: React_2.ComponentType<TemplateCardProps>; + }; + // (undocumented) + onSelected: (template: TemplateEntityV1beta3) => void; + // (undocumented) + templates: { + template: TemplateEntityV1beta3; + additionalLinks?: { + icon: IconComponent; + text: string; + url: string; + }[]; + }[]; + // (undocumented) + title: React_2.ReactNode; +} + +// @alpha +export const useFormDataFromQuery: ( + initialState?: Record<string, JsonValue>, +) => [Record<string, any>, Dispatch<SetStateAction<Record<string, any>>>]; + +// @alpha (undocumented) +export const useTemplateParameterSchema: (templateRef: string) => { + manifest: TemplateParameterSchema; + loading: boolean; + error: Error | undefined; +}; + +// @alpha +export const useTemplateSchema: (manifest: TemplateParameterSchema) => { + steps: ParsedTemplateSchema[]; +}; + +// @alpha (undocumented) +export const Workflow: (workflowProps: WorkflowProps) => JSX.Element | null; + +// @alpha (undocumented) +export type WorkflowProps = { + title?: string; + description?: string; + namespace: string; + templateName: string; + onError(error: Error | undefined): JSX.Element | null; +} & Pick< + StepperProps, + | 'extensions' + | 'FormProps' + | 'components' + | 'onCreate' + | 'initialState' + | 'layouts' +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index 85ba2c937a..384c4b80f2 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -7,27 +7,22 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; -import { Dispatch } from 'react'; import { Extension } from '@backstage/core-plugin-api'; import { FieldProps } from '@rjsf/core'; -import { FieldProps as FieldProps_2 } from '@rjsf/utils'; import { FieldValidation } from '@rjsf/core'; -import { FieldValidation as FieldValidation_2 } from '@rjsf/utils'; -import type { FormProps as FormProps_2 } from '@rjsf/core-v5'; -import { IconComponent } from '@backstage/core-plugin-api'; +import type { FormProps } from '@rjsf/core-v5'; import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; -import { ReactNode } from 'react'; -import { SetStateAction } from 'react'; +import { ScaffolderTask as ScaffolderTask_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderTaskOutput as ScaffolderTaskOutput_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderTaskStatus as ScaffolderTaskStatus_2 } from '@backstage/plugin-scaffolder-react'; +import { Step as Step_2 } from '@backstage/plugin-scaffolder-react'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskStep } from '@backstage/plugin-scaffolder-common'; -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { UIOptionsType } from '@rjsf/utils'; -import { UiSchema } from '@rjsf/utils'; // @public export type Action = { @@ -46,17 +41,6 @@ export type ActionExample = { example: string; }; -// @alpha -export const createFieldValidation: () => FieldValidation_2; - -// @alpha -export function createNextScaffolderFieldExtension< - TReturnValue = unknown, - TInputProps extends UIOptionsType = {}, ->( - options: NextFieldExtensionOptions<TReturnValue, TInputProps>, -): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>; - // @public export function createScaffolderFieldExtension< TReturnValue = unknown, @@ -65,6 +49,11 @@ export function createScaffolderFieldExtension< options: FieldExtensionOptions<TReturnValue, TInputProps>, ): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>; +// @public +export function createScaffolderLayout<TInputProps = unknown>( + options: LayoutOptions, +): Extension<LayoutComponent<TInputProps>>; + // @public export type CustomFieldExtensionSchema = { returnValue: JSONSchema7; @@ -80,15 +69,6 @@ export type CustomFieldValidator<TFieldReturnValue> = ( }, ) => void | Promise<void>; -// @alpha (undocumented) -export const EmbeddableWorkflow: (props: WorkflowProps) => JSX.Element; - -// @alpha -export const extractSchemaFromStep: (inputStep: JsonObject) => { - uiSchema: UiSchema; - schema: JsonObject; -}; - // @public export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; @@ -116,11 +96,21 @@ export type FieldExtensionOptions< schema?: CustomFieldExtensionSchema; }; -// @alpha -export type FormProps = Pick< - FormProps_2, - 'transformErrors' | 'noHtml5Validate' ->; +// @public +export type LayoutComponent<_TInputProps> = () => null; + +// @public +export interface LayoutOptions<P = any> { + // (undocumented) + component: LayoutTemplate<P>; + // (undocumented) + name: string; +} + +// @public +export type LayoutTemplate<T = any> = NonNullable< + FormProps<T>['uiSchema'] +>['ui:ObjectFieldTemplate']; // @public export type ListActionsResponse = Array<Action>; @@ -138,63 +128,6 @@ export type LogEvent = { taskId: string; }; -// @alpha -export type NextCustomFieldValidator<TFieldReturnValue> = ( - data: TFieldReturnValue, - field: FieldValidation_2, - context: { - apiHolder: ApiHolder; - formData: JsonObject; - }, -) => void | Promise<void>; - -// @alpha -export interface NextFieldExtensionComponentProps< - TFieldReturnValue, - TUiOptions = {}, -> extends PropsWithChildren<FieldProps_2<TFieldReturnValue>> { - // (undocumented) - uiSchema?: UiSchema<TFieldReturnValue> & { - 'ui:options'?: TUiOptions & UIOptionsType; - }; -} - -// @alpha -export type NextFieldExtensionOptions< - TFieldReturnValue = unknown, - TInputProps = unknown, -> = { - name: string; - component: ( - props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>, - ) => JSX.Element | null; - validation?: NextCustomFieldValidator<TFieldReturnValue>; - schema?: CustomFieldExtensionSchema; -}; - -// @alpha -export interface ParsedTemplateSchema { - // (undocumented) - description?: string; - // (undocumented) - mergedSchema: JsonObject; - // (undocumented) - schema: JsonObject; - // (undocumented) - title: string; - // (undocumented) - uiSchema: UiSchema; -} - -// @alpha -export const ReviewState: (props: ReviewStateProps) => JSX.Element; - -// @alpha -export type ReviewStateProps = { - schemas: ParsedTemplateSchema[]; - formState: JsonObject; -}; - // @public export interface ScaffolderApi { // (undocumented) @@ -276,6 +209,9 @@ export interface ScaffolderGetIntegrationsListResponse { }[]; } +// @public +export const ScaffolderLayouts: React.ComponentType; + // @public (undocumented) export type ScaffolderOutputLink = { title?: string; @@ -345,64 +281,52 @@ export const SecretsContextProvider: ({ children, }: PropsWithChildren<{}>) => JSX.Element; -// @alpha -export const Stepper: (stepperProps: StepperProps) => JSX.Element; - -// @alpha -export type StepperProps = { - manifest: TemplateParameterSchema; - extensions: NextFieldExtensionOptions<any, any>[]; - templateName?: string; - FormProps?: FormProps; - initialState?: Record<string, JsonValue>; - onCreate: (values: Record<string, JsonValue>) => Promise<void>; - components?: { - ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element; - createButtonText?: ReactNode; - reviewButtonText?: ReactNode; - }; +// @public +export type Step = { + id: string; + status: ScaffolderTaskStatus_2; + endedAt?: string; + startedAt?: string; }; -// @alpha -export const TemplateCard: (props: TemplateCardProps) => JSX.Element; - -// @alpha -export interface TemplateCardProps { +// @public (undocumented) +export interface StepperProps { // (undocumented) - additionalLinks?: { - icon: IconComponent; - text: string; - url: string; - }[]; + activeStep?: number; // (undocumented) - onSelected?: (template: TemplateEntityV1beta3) => void; - // (undocumented) - template: TemplateEntityV1beta3; + steps: (TaskStep & Step_2)[]; } -// @alpha -export const TemplateGroup: (props: TemplateGroupProps) => JSX.Element; +// @public +export const TaskBorder: (props: { + isComplete: boolean; + isError: boolean; +}) => JSX.Element; -// @alpha -export interface TemplateGroupProps { - // (undocumented) - components?: { - CardComponent?: React_2.ComponentType<TemplateCardProps>; +// @public +export const TaskLogStream: (props: { + logs: { + [k: string]: string[]; }; - // (undocumented) - onSelected: (template: TemplateEntityV1beta3) => void; - // (undocumented) - templates: { - template: TemplateEntityV1beta3; - additionalLinks?: { - icon: IconComponent; - text: string; - url: string; - }[]; - }[]; - // (undocumented) - title: React_2.ReactNode; -} +}) => JSX.Element; + +// @public +export const TaskSteps: (props: StepperProps) => JSX.Element; + +// @public +export type TaskStream = { + loading: boolean; + error?: Error; + stepLogs: { + [stepId in string]: string[]; + }; + completed: boolean; + task?: ScaffolderTask_2; + steps: { + [stepId in string]: Step; + }; + output?: ScaffolderTaskOutput_2; +}; // @public export type TemplateParameterSchema = { @@ -422,40 +346,16 @@ export const useCustomFieldExtensions: < outlet: React.ReactNode, ) => TComponentDataType[]; -// @alpha -export const useFormDataFromQuery: ( - initialState?: Record<string, JsonValue>, -) => [Record<string, any>, Dispatch<SetStateAction<Record<string, any>>>]; +// @public +export const useCustomLayouts: <TComponentDataType = LayoutOptions<any>>( + outlet: React.ReactNode, +) => TComponentDataType[]; -// @alpha (undocumented) -export const useTemplateParameterSchema: (templateRef: string) => { - manifest: TemplateParameterSchema | undefined; - loading: boolean; - error: Error | undefined; -}; - -// @alpha -export const useTemplateSchema: (manifest: TemplateParameterSchema) => { - steps: ParsedTemplateSchema[]; -}; +// @public +export const useTaskEventStream: (taskId: string) => TaskStream; // @public export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets; -// @alpha (undocumented) -export const Workflow: (workflowProps: WorkflowProps) => JSX.Element | null; - -// @alpha (undocumented) -export type WorkflowProps = { - title?: string; - description?: string; - namespace: string; - templateName: string; - onError(error: Error | undefined): JSX.Element | null; -} & Pick< - StepperProps, - 'extensions' | 'FormProps' | 'components' | 'onCreate' | 'initialState' ->; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 93ffd18d51..070a92ee53 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-scaffolder-react", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", - "version": "1.0.0", + "version": "1.1.1-next.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", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "web-library" @@ -24,7 +36,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -46,19 +58,24 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "@react-hookz/web": "^20.0.0", "@rjsf/core": "^3.2.1", - "@rjsf/core-v5": "npm:@rjsf/core@5.0.0-beta.16", + "@rjsf/core-v5": "npm:@rjsf/core@5.1.0", "@rjsf/material-ui": "^3.2.1", - "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.0.0-beta.16", - "@rjsf/utils": "5.0.0-beta.16", - "@rjsf/validator-ajv6": "5.0.0-beta.16", + "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.1.0", + "@rjsf/utils": "5.1.0", + "@rjsf/validator-ajv8": "5.1.0", "@types/json-schema": "^7.0.9", "classnames": "^2.2.6", + "humanize-duration": "^3.25.1", + "immer": "^9.0.1", "json-schema": "^0.4.0", "json-schema-library": "^7.3.9", "lodash": "^4.17.21", + "luxon": "^3.0.0", "qs": "^6.9.4", "react-use": "^17.2.4", + "use-immer": "^0.8.0", "zen-observable": "^0.10.0", "zod": "~3.18.0", "zod-to-json-schema": "~3.18.0" @@ -77,10 +94,11 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.0", - "@testing-library/user-event": "^14.0.0" + "@testing-library/user-event": "^14.0.0", + "@types/humanize-duration": "^3.18.1", + "@types/luxon": "^3.0.0" }, "files": [ - "dist", - "alpha" + "dist" ] } diff --git a/plugins/scaffolder-react/src/alpha.ts b/plugins/scaffolder-react/src/alpha.ts new file mode 100644 index 0000000000..731b8e07d9 --- /dev/null +++ b/plugins/scaffolder-react/src/alpha.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 './next'; +export type { FormProps } from './next'; diff --git a/plugins/scaffolder-react/src/components/TaskBorder/TaskBorder.test.tsx b/plugins/scaffolder-react/src/components/TaskBorder/TaskBorder.test.tsx new file mode 100644 index 0000000000..f3a59ad344 --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskBorder/TaskBorder.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { TaskBorder } from './TaskBorder'; +import { render } from '@testing-library/react'; + +describe('TaskBorder', () => { + it('should render a pending linear progress if the task is not complete', () => { + const { getByRole } = render( + <TaskBorder isComplete={false} isError={false} />, + ); + + const progressBar = getByRole('progressbar'); + + expect(progressBar).toBeInTheDocument(); + expect(progressBar).toHaveClass('MuiLinearProgress-indeterminate'); + }); + + it('should render a determinate progress bar if the task is complete', () => { + const { getByRole } = render(<TaskBorder isComplete isError />); + + const progressBar = getByRole('progressbar'); + + expect(progressBar).toBeInTheDocument(); + expect(progressBar).toHaveClass('MuiLinearProgress-determinate'); + }); +}); diff --git a/plugins/scaffolder-react/src/components/TaskBorder/TaskBorder.tsx b/plugins/scaffolder-react/src/components/TaskBorder/TaskBorder.tsx new file mode 100644 index 0000000000..17eea3997a --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskBorder/TaskBorder.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { LinearProgress, makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + failed: { + backgroundColor: theme.palette.error.main, + }, + success: { + backgroundColor: theme.palette.success.main, + }, +})); + +/** + * The visual progress of the task event stream + * + * @public + */ +export const TaskBorder = (props: { + isComplete: boolean; + isError: boolean; +}) => { + const styles = useStyles(); + + if (!props.isComplete) { + return <LinearProgress variant="indeterminate" />; + } + + return ( + <LinearProgress + variant="determinate" + classes={{ bar: props.isError ? styles.failed : styles.success }} + value={100} + /> + ); +}; diff --git a/plugins/scaffolder-react/src/components/TaskBorder/index.ts b/plugins/scaffolder-react/src/components/TaskBorder/index.ts new file mode 100644 index 0000000000..e1559957d1 --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskBorder/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TaskBorder } from './TaskBorder'; diff --git a/plugins/scaffolder-react/src/components/TaskLogStream/TaskLogStream.test.tsx b/plugins/scaffolder-react/src/components/TaskLogStream/TaskLogStream.test.tsx new file mode 100644 index 0000000000..e8f9e67e96 --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskLogStream/TaskLogStream.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderInTestApp } from '@backstage/test-utils'; +import React, { ReactNode } from 'react'; +import { TaskLogStream } from './TaskLogStream'; + +// The <AutoSizer> inside <LogViewer> needs mocking to render in jsdom +jest.mock('react-virtualized-auto-sizer', () => ({ + __esModule: true, + default: (props: { + children: (size: { width: number; height: number }) => ReactNode; + }) => <>{props.children({ width: 400, height: 200 })}</>, +})); + +describe('TaskLogStream', () => { + it('should render a log stream with the correct log lines', async () => { + const logs = { step: ['line 1', 'line 2'], step2: ['line 3'] }; + + const { findByText } = await renderInTestApp(<TaskLogStream logs={logs} />); + + const logLines = Object.values(logs).flat(); + + for (const line of logLines) { + await expect(findByText(line)).resolves.toBeInTheDocument(); + } + }); + + it('should filter out empty log lines', async () => { + const logs = { step: ['line 1', 'line 2'], step2: [] }; + + const { findAllByRole } = await renderInTestApp( + <TaskLogStream logs={logs} />, + ); + + await expect(findAllByRole('row')).resolves.toHaveLength(2); + }); +}); diff --git a/plugins/scaffolder-react/src/components/TaskLogStream/TaskLogStream.tsx b/plugins/scaffolder-react/src/components/TaskLogStream/TaskLogStream.tsx new file mode 100644 index 0000000000..973627aa02 --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskLogStream/TaskLogStream.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { LogViewer } from '@backstage/core-components'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles({ + root: { + width: '100%', + height: '100%', + position: 'relative', + }, +}); + +/** + * The text of the event stream + * + * @public + */ +export const TaskLogStream = (props: { logs: { [k: string]: string[] } }) => { + const styles = useStyles(); + return ( + <div className={styles.root}> + <LogViewer + text={Object.values(props.logs) + .map(l => l.join('\n')) + .filter(Boolean) + .join('\n')} + /> + </div> + ); +}; diff --git a/plugins/scaffolder-react/src/components/TaskLogStream/index.ts b/plugins/scaffolder-react/src/components/TaskLogStream/index.ts new file mode 100644 index 0000000000..a6570f996b --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskLogStream/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TaskLogStream } from './TaskLogStream'; diff --git a/plugins/scaffolder-react/src/components/TaskSteps/StepIcon.tsx b/plugins/scaffolder-react/src/components/TaskSteps/StepIcon.tsx new file mode 100644 index 0000000000..cecf682596 --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskSteps/StepIcon.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { BackstageTheme } from '@backstage/theme'; +import { CircularProgress, makeStyles, StepIconProps } from '@material-ui/core'; +import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline'; +import PanoramaFishEyeIcon from '@material-ui/icons/PanoramaFishEye'; +import classNames from 'classnames'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; +import ErrorOutline from '@material-ui/icons/ErrorOutline'; + +const useStepIconStyles = makeStyles((theme: BackstageTheme) => ({ + root: { + color: theme.palette.text.disabled, + }, + completed: { + color: theme.palette.status.ok, + }, + error: { + color: theme.palette.status.error, + }, +})); + +export const StepIcon = (props: StepIconProps & { skipped: boolean }) => { + const classes = useStepIconStyles(); + const { active, completed, error, skipped } = props; + + const getMiddle = () => { + if (active) { + return <CircularProgress size="20px" />; + } + if (completed) { + return <CheckCircleOutline />; + } + + if (error) { + return <ErrorOutline />; + } + + if (skipped) { + return <RemoveCircleOutline />; + } + + return <PanoramaFishEyeIcon />; + }; + + return ( + <div + className={classNames(classes.root, { + [classes.completed]: completed, + [classes.error]: error, + })} + > + {getMiddle()} + </div> + ); +}; diff --git a/plugins/scaffolder-react/src/components/TaskSteps/StepTime.test.tsx b/plugins/scaffolder-react/src/components/TaskSteps/StepTime.test.tsx new file mode 100644 index 0000000000..185d2fa7fb --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskSteps/StepTime.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { StepTime } from './StepTime'; +import { render, act } from '@testing-library/react'; + +describe('StepTime', () => { + it('should return empty when there is no start time', () => { + const step = { + id: 'test', + startedAt: undefined, + endedAt: undefined, + name: 'test', + }; + + const { container } = render(<StepTime step={step} />); + + expect(container.querySelector('span')?.textContent).toBe(''); + }); + + it('should format the time between the start and end date properly', async () => { + const step = { + id: 'test', + startedAt: '2021-01-01T00:00:00Z', + endedAt: '2021-01-01T00:00:01Z', + name: 'test', + }; + + const { findByText } = render(<StepTime step={step} />); + + await expect(findByText('1 second')).resolves.toBeInTheDocument(); + }); + + describe('updates', () => { + beforeEach(() => { + jest.useFakeTimers({ now: new Date('2021-01-01T00:00:00Z') }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should update the time every second', async () => { + const step = { + id: 'test', + startedAt: '2021-01-01T00:00:00Z', + endedAt: undefined, + name: 'test', + }; + + const { findByText } = render(<StepTime step={step} />); + + await expect(findByText('0 seconds')).resolves.toBeInTheDocument(); + + act(() => jest.advanceTimersByTime(1000)); + + await expect(findByText('1 second')).resolves.toBeInTheDocument(); + + act(() => jest.advanceTimersByTime(1000 * 60)); + + await expect( + findByText('1 minute, 1 second'), + ).resolves.toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/scaffolder-react/src/components/TaskSteps/StepTime.tsx b/plugins/scaffolder-react/src/components/TaskSteps/StepTime.tsx new file mode 100644 index 0000000000..653fe6e942 --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskSteps/StepTime.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useCallback, useState } from 'react'; +import useInterval from 'react-use/lib/useInterval'; +import { DateTime, Interval } from 'luxon'; +import humanizeDuration from 'humanize-duration'; +import { Typography } from '@material-ui/core'; +import { useMountEffect } from '@react-hookz/web'; + +export const StepTime = (props: { + step: { + id: string; + name: string; + startedAt?: string; + endedAt?: string; + }; +}) => { + const [time, setTime] = useState(''); + const { step } = props; + + const calculate = useCallback(() => { + if (!step.startedAt) { + setTime(''); + return; + } + + const end = step.endedAt + ? DateTime.fromISO(step.endedAt) + : DateTime.local(); + + const startedAt = DateTime.fromISO(step.startedAt); + const formatted = Interval.fromDateTimes(startedAt, end) + .toDuration() + .valueOf(); + + setTime(humanizeDuration(formatted, { round: true })); + }, [step.endedAt, step.startedAt]); + + useMountEffect(() => calculate()); + + useInterval(() => !step.endedAt && calculate(), 1000); + + return <Typography variant="caption">{time}</Typography>; +}; diff --git a/plugins/scaffolder-react/src/components/TaskSteps/TaskSteps.test.tsx b/plugins/scaffolder-react/src/components/TaskSteps/TaskSteps.test.tsx new file mode 100644 index 0000000000..afd1f58190 --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskSteps/TaskSteps.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { TaskSteps } from './TaskSteps'; +import { ScaffolderTaskStatus } from '@backstage/plugin-scaffolder-react'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('TaskSteps', () => { + it('should render each of the steps', async () => { + const steps = [ + { + id: '1', + name: 'Step 1', + status: 'processing' as ScaffolderTaskStatus, + startedAt: Date.now().toLocaleString(), + action: 'create', + }, + { + id: '2', + name: 'Step 2', + status: 'failed' as ScaffolderTaskStatus, + + startedAt: Date.now().toLocaleString(), + endedAt: Date.now().toLocaleString(), + action: 'create', + }, + { + id: '3', + name: 'Step 3', + status: 'completed' as ScaffolderTaskStatus, + startedAt: Date.now().toLocaleString(), + endedAt: Date.now().toLocaleString(), + action: 'create', + }, + { + id: '4', + name: 'Step 4', + status: 'skipped' as ScaffolderTaskStatus, + startedAt: Date.now().toLocaleString(), + endedAt: Date.now().toLocaleString(), + action: 'create', + }, + ]; + + const { getByText } = await renderInTestApp(<TaskSteps steps={steps} />); + + for (const step of steps) { + expect(getByText(step.name)).toBeInTheDocument(); + } + }); +}); diff --git a/plugins/scaffolder-react/src/components/TaskSteps/TaskSteps.tsx b/plugins/scaffolder-react/src/components/TaskSteps/TaskSteps.tsx new file mode 100644 index 0000000000..8b43d5e17b --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskSteps/TaskSteps.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 { + Stepper as MuiStepper, + Step as MuiStep, + StepButton as MuiStepButton, + StepLabel as MuiStepLabel, + StepIconProps, + Box, +} from '@material-ui/core'; +import { TaskStep } from '@backstage/plugin-scaffolder-common'; +import { type Step } from '@backstage/plugin-scaffolder-react'; +import { StepIcon } from './StepIcon'; +import { StepTime } from './StepTime'; + +/** + * + * @public + */ +export interface StepperProps { + steps: (TaskStep & Step)[]; + activeStep?: number; +} + +/** + * The visual stepper of the task event stream + * + * @public + */ +export const TaskSteps = (props: StepperProps) => { + return ( + <MuiStepper + activeStep={props.activeStep} + alternativeLabel + variant="elevation" + > + {props.steps.map((step, index) => { + const isCompleted = step.status === 'completed'; + const isFailed = step.status === 'failed'; + const isActive = step.status === 'processing'; + const isSkipped = step.status === 'skipped'; + const stepIconProps: Partial<StepIconProps & { skipped: boolean }> = { + completed: isCompleted, + error: isFailed, + active: isActive, + skipped: isSkipped, + }; + + return ( + <MuiStep key={index}> + <MuiStepButton> + <MuiStepLabel + StepIconProps={stepIconProps} + StepIconComponent={StepIcon} + > + <Box>{step.name}</Box> + <StepTime step={step} /> + </MuiStepLabel> + </MuiStepButton> + </MuiStep> + ); + })} + </MuiStepper> + ); +}; diff --git a/plugins/scaffolder-react/src/components/TaskSteps/index.ts b/plugins/scaffolder-react/src/components/TaskSteps/index.ts new file mode 100644 index 0000000000..91f5537bd3 --- /dev/null +++ b/plugins/scaffolder-react/src/components/TaskSteps/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TaskSteps, type StepperProps } from './TaskSteps'; diff --git a/plugins/scaffolder-react/src/components/index.ts b/plugins/scaffolder-react/src/components/index.ts new file mode 100644 index 0000000000..b724ed32b5 --- /dev/null +++ b/plugins/scaffolder-react/src/components/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TaskSteps, type StepperProps } from './TaskSteps'; +export { TaskBorder } from './TaskBorder'; +export { TaskLogStream } from './TaskLogStream'; diff --git a/plugins/scaffolder-react/src/extensions/index.tsx b/plugins/scaffolder-react/src/extensions/index.tsx index 33442f6697..d70f5e36c5 100644 --- a/plugins/scaffolder-react/src/extensions/index.tsx +++ b/plugins/scaffolder-react/src/extensions/index.tsx @@ -25,7 +25,8 @@ import { Extension, attachComponentData } from '@backstage/core-plugin-api'; import { FIELD_EXTENSION_KEY, FIELD_EXTENSION_WRAPPER_KEY } from './keys'; /** - * A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps + * The type used to wrap up the Layout and embed the input props + * * @public */ export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; diff --git a/plugins/scaffolder-react/src/hooks/index.ts b/plugins/scaffolder-react/src/hooks/index.ts index 022fe7c38c..124950e83d 100644 --- a/plugins/scaffolder-react/src/hooks/index.ts +++ b/plugins/scaffolder-react/src/hooks/index.ts @@ -15,3 +15,9 @@ */ export { useCustomFieldExtensions } from './useCustomFieldExtensions'; +export { useCustomLayouts } from './useCustomLayouts'; +export { + useTaskEventStream, + type TaskStream, + type Step, +} from './useEventStream'; diff --git a/plugins/scaffolder-react/src/hooks/useCustomLayouts.ts b/plugins/scaffolder-react/src/hooks/useCustomLayouts.ts new file mode 100644 index 0000000000..461b89f808 --- /dev/null +++ b/plugins/scaffolder-react/src/hooks/useCustomLayouts.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useElementFilter } from '@backstage/core-plugin-api'; +import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts/keys'; +import { LayoutOptions } from '../layouts'; + +/** + * Hook that returns all custom field extensions from the current outlet. + * @public + */ +export const useCustomLayouts = <TComponentDataType = LayoutOptions>( + outlet: React.ReactNode, +) => { + return useElementFilter(outlet, elements => + elements + .selectByComponentData({ + key: LAYOUTS_WRAPPER_KEY, + }) + .findComponentData<TComponentDataType>({ + key: LAYOUTS_KEY, + }), + ); +}; diff --git a/plugins/scaffolder/src/components/hooks/useEventStream.ts b/plugins/scaffolder-react/src/hooks/useEventStream.ts similarity index 96% rename from plugins/scaffolder/src/components/hooks/useEventStream.ts rename to plugins/scaffolder-react/src/hooks/useEventStream.ts index 0b36c85ad8..101063d258 100644 --- a/plugins/scaffolder/src/components/hooks/useEventStream.ts +++ b/plugins/scaffolder-react/src/hooks/useEventStream.ts @@ -25,13 +25,23 @@ import { import { useApi } from '@backstage/core-plugin-api'; import { Subscription } from '@backstage/types'; -type Step = { +/** + * The status of the step being processed + * + * @public + */ +export type Step = { id: string; status: ScaffolderTaskStatus; endedAt?: string; startedAt?: string; }; +/** + * A task event from the event stream + * + * @public + */ export type TaskStream = { loading: boolean; error?: Error; @@ -132,6 +142,11 @@ function reducer(draft: TaskStream, action: ReducerAction) { } } +/** + * A hook to stream the logs of a task being processed + * + * @public + */ export const useTaskEventStream = (taskId: string): TaskStream => { const scaffolderApi = useApi(scaffolderApiRef); const [state, dispatch] = useImmerReducer(reducer, { diff --git a/plugins/scaffolder-react/src/index.ts b/plugins/scaffolder-react/src/index.ts index 41a1298b37..38584ab25b 100644 --- a/plugins/scaffolder-react/src/index.ts +++ b/plugins/scaffolder-react/src/index.ts @@ -19,6 +19,5 @@ export * from './types'; export * from './secrets'; export * from './api'; export * from './hooks'; - -export * from './next'; -export type { FormProps } from './next'; +export * from './layouts'; +export * from './components'; diff --git a/plugins/scaffolder/src/layouts/index.tsx b/plugins/scaffolder-react/src/layouts/createScaffolderLayout.ts similarity index 67% rename from plugins/scaffolder/src/layouts/index.tsx rename to plugins/scaffolder-react/src/layouts/createScaffolderLayout.ts index ce1dac9bdf..328f366007 100644 --- a/plugins/scaffolder/src/layouts/index.tsx +++ b/plugins/scaffolder-react/src/layouts/createScaffolderLayout.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2023 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,17 +14,33 @@ * limitations under the License. */ +import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from './keys'; import { attachComponentData, Extension } from '@backstage/core-plugin-api'; -import type { LayoutOptions } from './types'; - -export const LAYOUTS_KEY = 'scaffolder.layout.v1'; -export const LAYOUTS_WRAPPER_KEY = 'scaffolder.layouts.wrapper.v1'; +import type { FormProps as SchemaFormProps } from '@rjsf/core-v5'; /** - * The type used to wrap up the Layout and embed the input props + * The field template from `@rjsf/core` which is a react component that gets passed `@rjsf/core` field related props. * * @public */ +export type LayoutTemplate<T = any> = NonNullable< + SchemaFormProps<T>['uiSchema'] +>['ui:ObjectFieldTemplate']; + +/** + * The type of layouts that is passed to the TemplateForms + * + * @public + */ +export interface LayoutOptions<P = any> { + name: string; + component: LayoutTemplate<P>; +} + +/** + * A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps + * @public + */ export type LayoutComponent<_TInputProps> = () => null; /** @@ -55,5 +71,3 @@ export const ScaffolderLayouts: React.ComponentType = (): JSX.Element | null => null; attachComponentData(ScaffolderLayouts, LAYOUTS_WRAPPER_KEY, true); - -export type { LayoutOptions, LayoutTemplate } from './types'; diff --git a/plugins/scaffolder-react/src/layouts/index.tsx b/plugins/scaffolder-react/src/layouts/index.tsx new file mode 100644 index 0000000000..ab3c30e1cb --- /dev/null +++ b/plugins/scaffolder-react/src/layouts/index.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + ScaffolderLayouts, + createScaffolderLayout, +} from './createScaffolderLayout'; +export type { + LayoutComponent, + LayoutTemplate, + LayoutOptions, +} from './createScaffolderLayout'; diff --git a/plugins/scaffolder-react/src/layouts/keys.ts b/plugins/scaffolder-react/src/layouts/keys.ts new file mode 100644 index 0000000000..a82b1597f4 --- /dev/null +++ b/plugins/scaffolder-react/src/layouts/keys.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const LAYOUTS_KEY = 'scaffolder.layout.v1'; +export const LAYOUTS_WRAPPER_KEY = 'scaffolder.layouts.wrapper.v1'; diff --git a/packages/backend-test-utils/src/next/implementations/mockConfigService.ts b/plugins/scaffolder-react/src/next/components/Form/Form.tsx similarity index 59% rename from packages/backend-test-utils/src/next/implementations/mockConfigService.ts rename to plugins/scaffolder-react/src/next/components/Form/Form.tsx index 1d04faad91..79bcaa8475 100644 --- a/packages/backend-test-utils/src/next/implementations/mockConfigService.ts +++ b/plugins/scaffolder-react/src/next/components/Form/Form.tsx @@ -14,20 +14,10 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { ConfigReader } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; +import { withTheme } from '@rjsf/core-v5'; +// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly +// which is what we're using here as the default types, it needs to depend on @rjsf/core-v5 because +// of the re-writing we're doing. Once we've migrated, we can import this the exact same as before. /** @alpha */ -export const mockConfigFactory = createServiceFactory( - (options?: { data?: JsonObject }) => ({ - service: coreServices.config, - deps: {}, - async factory() { - return new ConfigReader(options?.data, 'mock-config'); - }, - }), -); +export const Form = withTheme(require('@rjsf/material-ui-v5').Theme); diff --git a/plugins/scaffolder-react/src/next/components/Form/index.ts b/plugins/scaffolder-react/src/next/components/Form/index.ts new file mode 100644 index 0000000000..6f780e92a4 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Form/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 { Form } from './Form'; diff --git a/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/DescriptionField.tsx b/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/DescriptionField.tsx new file mode 100644 index 0000000000..eded38beee --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/DescriptionField.tsx @@ -0,0 +1,22 @@ +/* + * 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 { MarkdownContent } from '@backstage/core-components'; +import { FieldProps } from '@rjsf/utils'; + +export const DescriptionField = ({ description }: FieldProps) => + description && <MarkdownContent content={description} linkTarget="_blank" />; diff --git a/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/index.ts b/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/index.ts new file mode 100644 index 0000000000..d6d0bf3499 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { DescriptionField } from './DescriptionField'; diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index 5dbce3243f..0b17c550a0 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -21,6 +21,7 @@ import { act, fireEvent } from '@testing-library/react'; import type { RJSFValidationError } from '@rjsf/utils'; import { JsonValue } from '@backstage/types'; import { NextFieldExtensionComponentProps } from '../../extensions'; +import { LayoutTemplate } from '../../../layouts'; describe('Stepper', () => { it('should render the step titles for each step of the manifest', async () => { @@ -392,4 +393,47 @@ describe('Stepper', () => { await fireEvent.click(getByRole('button', { name: 'Make' })); }); }); + + describe('Scaffolder Layouts', () => { + it('should render the step in the scaffolder layout', async () => { + const ScaffolderLayout: LayoutTemplate = ({ properties }) => ( + <> + <h1>A Scaffolder Layout</h1> + {properties.map((prop, i) => ( + <div key={i}>{prop.content}</div> + ))} + </> + ); + + const manifest: TemplateParameterSchema = { + steps: [ + { + title: 'Step 1', + schema: { + type: 'object', + 'ui:ObjectFieldTemplate': 'Layout', + properties: { + field1: { + type: 'string', + }, + }, + }, + }, + ], + title: 'scaffolder layouts', + }; + + const { getByText, getByRole } = await renderInTestApp( + <Stepper + manifest={manifest} + extensions={[]} + onCreate={jest.fn()} + layouts={[{ name: 'Layout', component: ScaffolderLayout }]} + />, + ); + + expect(getByText('A Scaffolder Layout')).toBeInTheDocument(); + expect(getByRole('textbox', { name: 'field1' })).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 950c9720de..444e463064 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -22,17 +22,25 @@ import { Button, makeStyles, } from '@material-ui/core'; -import { type IChangeEvent, withTheme } from '@rjsf/core-v5'; -import { ErrorSchema, FieldValidation } from '@rjsf/utils'; +import { type IChangeEvent } from '@rjsf/core-v5'; +import { ErrorSchema } from '@rjsf/utils'; import React, { useCallback, useMemo, useState, type ReactNode } from 'react'; import { NextFieldExtensionOptions } from '../../extensions'; -import { TemplateParameterSchema } from '../../../types'; -import { createAsyncValidators } from './createAsyncValidators'; -import type { FormProps } from '../../types'; +import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; +import { + createAsyncValidators, + type FormValidation, +} from './createAsyncValidators'; import { ReviewState, type ReviewStateProps } from '../ReviewState'; import { useTemplateSchema } from '../../hooks/useTemplateSchema'; -import { useFormDataFromQuery } from '../../hooks/useFormDataFromQuery'; -import validator from '@rjsf/validator-ajv6'; +import validator from '@rjsf/validator-ajv8'; +import { useFormDataFromQuery } from '../../hooks'; +import { FormProps } from '../../types'; +import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; +import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps'; +import { hasErrors } from './utils'; +import * as FieldOverrides from './FieldOverrides'; +import { Form } from '../Form'; const useStyles = makeStyles(theme => ({ backButton: { @@ -65,35 +73,27 @@ export type StepperProps = { createButtonText?: ReactNode; reviewButtonText?: ReactNode; }; + layouts?: LayoutOptions[]; }; -// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly -// which is what we're using here as the default types, it needs to depend on @rjsf/core-v5 because -// of the re-writing we're doing. Once we've migrated, we can import this the exact same as before. -const Form = withTheme(require('@rjsf/material-ui-v5').Theme); - /** * The `Stepper` component is the Wizard that is rendered when a user selects a template * @alpha */ - export const Stepper = (stepperProps: StepperProps) => { - const { components = {}, ...props } = stepperProps; + const { layouts = [], components = {}, ...props } = stepperProps; const { ReviewStateComponent = ReviewState, createButtonText = 'Create', reviewButtonText = 'Review', } = components; - const analytics = useAnalytics(); const { steps } = useTemplateSchema(props.manifest); const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); const [formState, setFormState] = useFormDataFromQuery(props.initialState); - const [errors, setErrors] = useState< - undefined | Record<string, FieldValidation> - >(); + const [errors, setErrors] = useState<undefined | FormValidation>(); const styles = useStyles(); const extensions = useMemo(() => { @@ -124,10 +124,12 @@ export const Stepper = (stepperProps: StepperProps) => { [setFormState], ); + const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); + const handleNext = async ({ - formData, + formData = {}, }: { - formData: Record<string, JsonValue>; + formData?: Record<string, JsonValue>; }) => { // TODO(blam): What do we do about loading states, does each field extension get a chance // to display it's own loading? Or should we grey out the entire form. @@ -135,11 +137,7 @@ export const Stepper = (stepperProps: StepperProps) => { const returnedValidation = await validation(formData); - const hasErrors = Object.values(returnedValidation).some( - i => i.__errors?.length, - ); - - if (hasErrors) { + if (hasErrors(returnedValidation)) { setErrors(returnedValidation); } else { setErrors(undefined); @@ -171,10 +169,10 @@ export const Stepper = (stepperProps: StepperProps) => { extraErrors={errors as unknown as ErrorSchema} formData={formState} formContext={{ formData: formState }} - schema={steps[activeStep].schema} - uiSchema={steps[activeStep].uiSchema} + schema={currentStep.schema} + uiSchema={currentStep.uiSchema} onSubmit={handleNext} - fields={extensions} + fields={{ ...FieldOverrides, ...extensions }} showErrorList={false} onChange={handleChange} {...(props.FormProps ?? {})} diff --git a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts index 6ad14e3cc4..196bae0255 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts @@ -56,6 +56,79 @@ describe('createAsyncValidators', () => { expect(validators.AddressField).toHaveBeenCalled(); }); + it('should call the validator function with the correct schema in the context', async () => { + const schema: JsonObject = { + type: 'object', + properties: { + name: { + type: 'string', + 'ui:options': { + bob: true, + }, + pattern: 'lols', + 'ui:field': 'NameField', + }, + address: { + type: 'object', + 'ui:field': 'AddressField', + properties: { + street: { + type: 'string', + }, + postcode: { + type: 'string', + }, + }, + }, + }, + }; + + const validators = { NameField: jest.fn(), AddressField: jest.fn() }; + + const validate = createAsyncValidators(schema, validators, { + apiHolder: { get: jest.fn() }, + }); + + await validate({ + name: 'asd', + address: { street: 'street', postcode: 'postcode' }, + }); + + expect(validators.NameField).toHaveBeenCalledWith( + 'asd', + expect.anything(), + expect.objectContaining({ + schema: { + type: 'string', + 'ui:options': { + bob: true, + }, + pattern: 'lols', + 'ui:field': 'NameField', + }, + }), + ); + + expect(validators.AddressField).toHaveBeenCalledWith( + { street: 'street', postcode: 'postcode' }, + expect.anything(), + expect.objectContaining({ + schema: { + type: 'object', + 'ui:field': 'AddressField', + properties: { + street: { + type: 'string', + }, + postcode: { + type: 'string', + }, + }, + }, + }), + ); + }); + it('should return the correct errors to the frontend', async () => { const schema: JsonObject = { type: 'object', @@ -154,4 +227,177 @@ describe('createAsyncValidators', () => { }), }); }); + + it('should run validation on complex nested schemas', async () => { + const schema: JsonObject = { + title: 'Select component', + properties: { + actionType: { + title: 'action type', + type: 'string', + description: 'Select the action type', + enum: ['newThing', 'existingThing'], + enumNames: ['New thing', 'Existing thing'], + default: 'newThing', + }, + }, + required: ['actionType'], + dependencies: { + actionType: { + oneOf: [ + { + properties: { + actionType: { + enum: ['newThing'], + }, + general: { + title: 'General', + type: 'object', + properties: { + address: { + type: 'object', + 'ui:field': 'AddressField', + properties: { + street: { + type: 'string', + }, + postcode: { + type: 'string', + }, + }, + }, + name: { + title: 'Name', + type: 'string', + 'ui:field': 'NameField', + }, + }, + }, + }, + }, + { + properties: { + actionType: { + enum: ['existingThing'], + }, + thingId: { + title: 'Thing id', + type: 'string', + description: 'Enter thing id', + }, + }, + }, + ], + }, + }, + }; + + const AddressField: NextCustomFieldValidator<{ + street?: string; + postcode?: string; + }> = (value, { addError }) => { + if (!value.postcode) { + addError('postcode is missing!'); + } + + if (!value.street) { + addError('street is missing here!'); + } + }; + + const NameField: NextCustomFieldValidator<string> = ( + value, + { addError }, + ) => { + if (!value) { + addError('something is broken here!'); + } + }; + + const validators = { + AddressField: AddressField as NextCustomFieldValidator<unknown>, + NameField: NameField as NextCustomFieldValidator<unknown>, + }; + + const validate = createAsyncValidators(schema, validators, { + apiHolder: { get: jest.fn() }, + }); + + await expect( + validate({ + actionType: 'newThing', + general: { + address: { + street: 'street', + postcode: 'postcode', + }, + name: undefined, + }, + }), + ).resolves.toEqual({ + general: { + address: expect.objectContaining({ + __errors: [], + }), + name: expect.objectContaining({ + __errors: ['something is broken here!'], + }), + }, + }); + }); + + it('should call a validator for array property from a custom field extension', async () => { + const schema: JsonObject = { + type: 'object', + properties: { + tags: { + title: 'Tags', + type: 'array', + items: { + type: 'string', + 'ui:field': 'TagField', + }, + }, + }, + }; + + const validators = { TagField: jest.fn() }; + + const validate = createAsyncValidators(schema, validators, { + apiHolder: { get: jest.fn() }, + }); + + await validate({ + tags: ['tag-1', 'tag-2'], + }); + + expect(validators.TagField).toHaveBeenCalled(); + }); + + it('should does not call a validator if no ui field specified', async () => { + const schema: JsonObject = { + type: 'object', + properties: { + tags: { + title: 'Tags', + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + const validators = { TagField: jest.fn() }; + + const validate = createAsyncValidators(schema, validators, { + apiHolder: { get: jest.fn() }, + }); + + await validate({ + tags: ['asd', 'asd$'], + }); + + expect(validators.TagField).not.toHaveBeenCalled(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts index 79466ee602..111f3a0f1f 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts @@ -15,11 +15,19 @@ */ import { FieldValidation } from '@rjsf/utils'; -import { JsonObject } from '@backstage/types'; +import type { JsonObject, JsonValue } from '@backstage/types'; import { ApiHolder } from '@backstage/core-plugin-api'; import { Draft07 as JSONSchema } from 'json-schema-library'; import { createFieldValidation } from '../../lib'; import { NextCustomFieldValidator } from '../../extensions'; +import { isObject } from './utils'; + +/** + * @internal + */ +export type FormValidation = { + [name: string]: FieldValidation | FormValidation; +}; export const createAsyncValidators = ( rootSchema: JsonObject, @@ -28,26 +36,64 @@ export const createAsyncValidators = ( apiHolder: ApiHolder; }, ) => { - async function validate(formData: JsonObject, pathPrefix: string = '#') { + async function validate( + formData: JsonObject, + pathPrefix: string = '#', + current: JsonObject = formData, + ): Promise<FormValidation> { const parsedSchema = new JSONSchema(rootSchema); - const formValidation: Record<string, FieldValidation> = {}; - for (const [key, value] of Object.entries(formData)) { - const definitionInSchema = parsedSchema.getSchema( - `${pathPrefix}/${key}`, - formData, - ); + const formValidation: FormValidation = {}; + + const validateForm = async ( + validatorName: string, + key: string, + value: JsonValue | undefined, + schema: JsonObject, + ) => { + const validator = validators[validatorName]; + if (validator) { + const fieldValidation = createFieldValidation(); + try { + await validator(value, fieldValidation, { + ...context, + formData, + schema, + }); + } catch (ex) { + fieldValidation.addError(ex.message); + } + formValidation[key] = fieldValidation; + } + }; + + for (const [key, value] of Object.entries(current)) { + const path = `${pathPrefix}/${key}`; + const definitionInSchema = parsedSchema.getSchema(path, formData); if (definitionInSchema && 'ui:field' in definitionInSchema) { - const validator = validators[definitionInSchema['ui:field']]; - if (validator) { - const fieldValidation = createFieldValidation(); - try { - await validator(value, fieldValidation, { ...context, formData }); - } catch (ex) { - fieldValidation.addError(ex.message); - } - formValidation[key] = fieldValidation; + if ('ui:field' in definitionInSchema) { + await validateForm( + definitionInSchema['ui:field'], + key, + value, + definitionInSchema, + ); } + } else if ( + definitionInSchema && + definitionInSchema.items && + 'ui:field' in definitionInSchema.items + ) { + if ('ui:field' in definitionInSchema.items) { + await validateForm( + definitionInSchema.items['ui:field'], + key, + value, + definitionInSchema.items, + ); + } + } else if (isObject(value)) { + formValidation[key] = await validate(formData, path, value); } } diff --git a/plugins/scaffolder-react/src/next/components/Stepper/utils.test.ts b/plugins/scaffolder-react/src/next/components/Stepper/utils.test.ts new file mode 100644 index 0000000000..24c62ebb35 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/utils.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { hasErrors } from './utils'; + +describe('hasErrors', () => { + it('should return false for empty _errors', () => { + expect( + hasErrors({ + name: { + __errors: [], + addError: jest.fn(), + }, + }), + ).toBe(false); + }); + + it('should return true for a single error', () => { + expect( + hasErrors({ + name: { + __errors: ['an error'], + addError: jest.fn(), + }, + }), + ).toBe(true); + }); + + it('should return true for more than one error', () => { + expect( + hasErrors({ + name: { + __errors: [], + addError: jest.fn(), + }, + general: { + address: { + __errors: [], + addError: jest.fn(), + }, + name: { + __errors: ['something is broken here!'], + addError: jest.fn(), + }, + }, + }), + ).toBe(true); + }); + + it('should not return false when the error is an empty object', () => { + const errors = { + something: {}, + otherThing: {}, + someName: { + __errors: [ + 'Accepts alphanumeric values along with _(underscore) and -(hypen) as special characters', + ], + addError: jest.fn(), + }, + someOtherName: { + __errors: ['Must start with an alphabet & not contain .(period)'], + addError: jest.fn(), + }, + aName: { + __errors: [], + addError: jest.fn(), + }, + bName: { + __errors: [], + addError: jest.fn(), + }, + cName: { + __errors: [], + addError: jest.fn(), + }, + }; + + expect(hasErrors(errors)).toBe(true); + }); +}); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/utils.ts b/plugins/scaffolder-react/src/next/components/Stepper/utils.ts new file mode 100644 index 0000000000..0b70f57813 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/utils.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { JsonObject, JsonValue } from '@backstage/types'; +import type { FieldValidation } from '@rjsf/utils'; +import { FormValidation } from './createAsyncValidators'; + +function isFieldValidation(error: any): error is FieldValidation { + return !!error && '__errors' in error; +} + +export function hasErrors(errors?: FormValidation): boolean { + if (!errors) { + return false; + } + + for (const error of Object.values(errors)) { + if (isFieldValidation(error)) { + if ((error.__errors ?? []).length > 0) { + return true; + } + + continue; + } + + if (hasErrors(error)) { + return true; + } + } + + return false; +} + +export function isObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx index 6f89808405..af44085151 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx @@ -162,7 +162,7 @@ export const TemplateCard = (props: TemplateCardProps) => { )} </Grid> </CardContent> - <CardActions style={{ padding: '16px' }}> + <CardActions style={{ padding: '16px', flex: 1, alignItems: 'flex-end' }}> <div className={styles.footer}> <div className={styles.ownedBy}> {ownedByRelations.length > 0 && ( diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx new file mode 100644 index 0000000000..4d908461b0 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Box, Paper } from '@material-ui/core'; +import { LinkOutputs } from './LinkOutputs'; +import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; + +/** + * The DefaultOutputs renderer for the scaffolder task output + * + * @alpha + */ +export const DefaultTemplateOutputs = (props: { + output?: ScaffolderTaskOutput; +}) => { + if (!props.output?.links) { + return null; + } + + return ( + <Box paddingBottom={2}> + <Paper> + <Box padding={2} justifyContent="center" display="flex" gridGap={16}> + <LinkOutputs output={props.output} /> + </Box> + </Paper> + </Box> + ); +}; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/LinkOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/LinkOutputs.tsx new file mode 100644 index 0000000000..d172aa9cb3 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/LinkOutputs.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api'; +import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { Button, makeStyles } from '@material-ui/core'; +import React from 'react'; +import WebIcon from '@material-ui/icons/Web'; +import { parseEntityRef } from '@backstage/catalog-model'; +import { Link } from '@backstage/core-components'; +import { ScaffolderTaskOutput } from '../../../api'; + +const useStyles = makeStyles({ + root: { + '&:hover': { + textDecoration: 'none', + }, + }, +}); + +export const LinkOutputs = (props: { output: ScaffolderTaskOutput }) => { + const { links = [] } = props.output; + const classes = useStyles(); + const app = useApp(); + const entityRoute = useRouteRef(entityRouteRef); + + const iconResolver = (key?: string): IconComponent => + app.getSystemIcon(key!) ?? WebIcon; + + return ( + <> + {links + .filter(({ url, entityRef }) => url || entityRef) + .map(({ url, entityRef, title, icon }) => { + if (entityRef) { + const entityName = parseEntityRef(entityRef); + const target = entityRoute(entityName); + return { title, icon, url: target }; + } + return { title, icon, url: url! }; + }) + .map(({ url, title, icon }, i) => { + const Icon = iconResolver(icon); + return ( + <Link to={url} key={i} classes={{ root: classes.root }}> + <Button startIcon={<Icon />} component="div" color="primary"> + {title} + </Button> + </Link> + ); + })} + </> + ); +}; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/index.ts b/plugins/scaffolder-react/src/next/components/TemplateOutputs/index.ts new file mode 100644 index 0000000000..1c998eb1b9 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { DefaultTemplateOutputs } from './DefaultTemplateOutputs'; diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx index cca69ab7b3..f6ff12e3a3 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx @@ -51,7 +51,12 @@ export type WorkflowProps = { onError(error: Error | undefined): JSX.Element | null; } & Pick< StepperProps, - 'extensions' | 'FormProps' | 'components' | 'onCreate' | 'initialState' + | 'extensions' + | 'FormProps' + | 'components' + | 'onCreate' + | 'initialState' + | 'layouts' >; /** diff --git a/plugins/scaffolder-react/src/next/components/index.ts b/plugins/scaffolder-react/src/next/components/index.ts index babe3b4dcb..60a8fd9a51 100644 --- a/plugins/scaffolder-react/src/next/components/index.ts +++ b/plugins/scaffolder-react/src/next/components/index.ts @@ -18,3 +18,5 @@ export * from './TemplateCard'; export * from './ReviewState'; export * from './TemplateGroup'; export * from './Workflow'; +export * from './TemplateOutputs'; +export * from './Form'; diff --git a/plugins/scaffolder-react/src/next/extensions/index.tsx b/plugins/scaffolder-react/src/next/extensions/index.tsx index ee9b1cd326..b2d402c93f 100644 --- a/plugins/scaffolder-react/src/next/extensions/index.tsx +++ b/plugins/scaffolder-react/src/next/extensions/index.tsx @@ -21,7 +21,8 @@ import { } from './types'; import { Extension, attachComponentData } from '@backstage/core-plugin-api'; import { UIOptionsType } from '@rjsf/utils'; -import { FieldExtensionComponent } from '../../extensions'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { FieldExtensionComponent } from '@backstage/plugin-scaffolder-react'; import { FIELD_EXTENSION_KEY } from '../../extensions/keys'; /** diff --git a/plugins/scaffolder-react/src/next/extensions/types.ts b/plugins/scaffolder-react/src/next/extensions/types.ts index 27778b0420..0917efe85d 100644 --- a/plugins/scaffolder-react/src/next/extensions/types.ts +++ b/plugins/scaffolder-react/src/next/extensions/types.ts @@ -22,7 +22,7 @@ import { } from '@rjsf/utils'; import { PropsWithChildren } from 'react'; import { JsonObject } from '@backstage/types'; -import { CustomFieldExtensionSchema } from '../../extensions'; +import { CustomFieldExtensionSchema } from '@backstage/plugin-scaffolder-react'; /** * Type for Field Extension Props for RJSF v5 @@ -46,7 +46,11 @@ export interface NextFieldExtensionComponentProps< export type NextCustomFieldValidator<TFieldReturnValue> = ( data: TFieldReturnValue, field: FieldValidationV5, - context: { apiHolder: ApiHolder; formData: JsonObject }, + context: { + apiHolder: ApiHolder; + formData: JsonObject; + schema: JsonObject; + }, ) => void | Promise<void>; /** diff --git a/plugins/scaffolder-react/src/next/hooks/useTemplateParameterSchema.ts b/plugins/scaffolder-react/src/next/hooks/useTemplateParameterSchema.ts index 36b8b1e0bc..d060b9a09f 100644 --- a/plugins/scaffolder-react/src/next/hooks/useTemplateParameterSchema.ts +++ b/plugins/scaffolder-react/src/next/hooks/useTemplateParameterSchema.ts @@ -1,5 +1,3 @@ -import { useApi } from '@backstage/core-plugin-api'; - /* * Copyright 2023 The Backstage Authors * @@ -15,8 +13,11 @@ import { useApi } from '@backstage/core-plugin-api'; * See the License for the specific language governing permissions and * limitations under the License. */ + import useAsync from 'react-use/lib/useAsync'; import { scaffolderApiRef } from '../../api/ref'; +import { useApi } from '@backstage/core-plugin-api'; +import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; /** * @alpha @@ -28,5 +29,5 @@ export const useTemplateParameterSchema = (templateRef: string) => { [scaffolderApi, templateRef], ); - return { manifest: value, loading, error }; + return { manifest: value as TemplateParameterSchema, loading, error }; }; diff --git a/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.test.tsx b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.test.tsx index 153c48468d..ab33652338 100644 --- a/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.test.tsx +++ b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.test.tsx @@ -18,7 +18,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { featureFlagsApiRef } from '@backstage/core-plugin-api'; -import { TemplateParameterSchema } from '../../types'; +import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; describe('useTemplateSchema', () => { it('should generate the correct schema', () => { diff --git a/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts index 2b2e48cf68..26980b15cf 100644 --- a/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts +++ b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts @@ -16,7 +16,7 @@ import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { UiSchema } from '@rjsf/utils'; -import { TemplateParameterSchema } from '../../types'; +import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; import { extractSchemaFromStep } from '../lib'; /** diff --git a/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.test.tsx b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.test.tsx new file mode 100644 index 0000000000..6dba7b9f29 --- /dev/null +++ b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { TestApiProvider } from '@backstage/test-utils'; +import { type ParsedTemplateSchema } from './useTemplateSchema'; +import { useTransformSchemaToProps } from './useTransformSchemaToProps'; + +describe('useTransformSchemaToProps', () => { + it('should replace ui:ObjectFieldTemplate with actual component', () => { + const layouts = [{ name: 'TwoColumn', component: jest.fn() }]; + + const step: ParsedTemplateSchema = { + title: 'Fill in some steps', + mergedSchema: {}, + schema: {}, + uiSchema: { + 'ui:ObjectFieldTemplate': 'TwoColumn' as any, + name: { + 'ui:field': 'EntityNamePicker', + 'ui:autofocus': true, + }, + }, + }; + + const { result } = renderHook( + () => useTransformSchemaToProps(step, { layouts }), + { + wrapper: ({ children }) => ( + <TestApiProvider apis={[]}>{children}</TestApiProvider> + ), + }, + ); + + const { uiSchema } = result.current; + + expect(uiSchema['ui:ObjectFieldTemplate']).toEqual(layouts[0].component); + }); +}); diff --git a/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts new file mode 100644 index 0000000000..612ad5427b --- /dev/null +++ b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { type ParsedTemplateSchema } from './useTemplateSchema'; +import { type LayoutOptions } from '@backstage/plugin-scaffolder-react'; + +interface Options { + layouts?: LayoutOptions[]; +} + +export const useTransformSchemaToProps = ( + step: ParsedTemplateSchema, + options: Options = {}, +): ParsedTemplateSchema => { + const { layouts = [] } = options; + const objectFieldTemplate = step?.uiSchema['ui:ObjectFieldTemplate'] as + | string + | undefined; + + if (typeof objectFieldTemplate !== 'string') { + return step; + } + + const Layout = layouts.find( + layout => layout.name === objectFieldTemplate, + )?.component; + + if (!Layout) { + return step; + } + + return { + ...step, + uiSchema: { + ...step.uiSchema, + ['ui:ObjectFieldTemplate']: Layout, + }, + }; +}; diff --git a/plugins/scaffolder-react/src/next/index.ts b/plugins/scaffolder-react/src/next/index.ts index ea009ea556..a18649f506 100644 --- a/plugins/scaffolder-react/src/next/index.ts +++ b/plugins/scaffolder-react/src/next/index.ts @@ -15,6 +15,6 @@ */ export * from './components'; export * from './extensions'; -export type { FormProps } from './types'; +export * from './types'; export * from './lib'; export * from './hooks'; diff --git a/plugins/scaffolder-react/src/next/types.ts b/plugins/scaffolder-react/src/next/types.ts index 32d0fe85f0..192fdf4146 100644 --- a/plugins/scaffolder-react/src/next/types.ts +++ b/plugins/scaffolder-react/src/next/types.ts @@ -13,8 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import type { FormProps as SchemaFormProps } from '@rjsf/core-v5'; +// TODO(Rugvip): The FormProps type is actually supposed to be alpha, but since we want to +// refer to it from @backstage/plugin-scaffolder, it needs to be public for now. +// Once we support internal alpha re-exports this should be switched to an alpha export. + /** * Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage` * diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 0d25838d22..2c12e00c10 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,147 @@ # @backstage/plugin-scaffolder +## 1.11.1-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/plugin-scaffolder-react@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-react@0.4.11-next.0 + - @backstage/plugin-scaffolder-common@1.2.6-next.0 + +## 1.11.0 + +### Minor Changes + +- d6bb12a2e7: - **Deprecation** - Deprecated the following exports, please import them directly from `@backstage/plugin-scaffolder-react` instead + + ``` + createScaffolderLayout + ScaffolderLayouts + LayoutOptions + LayoutTemplate + ``` + +- a521379688: Migrating the `TemplateEditorPage` to work with the new components from `@backstage/plugin-scaffolder-react` +- 074f7e81b5: Added a missing validator check for items in an array +- 8c2966536b: Embed scaffolder workflow in other components + +### Patch Changes + +- 04f717a8e1: `scaffolder/next`: bump `react-jsonschema-form` libraries to `v5-stable` +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- cbab8ac107: lock versions of `@rjsf/*-beta` packages +- 346d6b6630: Upgrade `@rjsf` version 5 dependencies to `beta.18` +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- d2ddde2108: Add `ScaffolderLayouts` to `NextScaffolderPage` +- 0f0da2f256: Prefer schema ordering of template properties during review content generation. +- 38992bdbaf: Fixed bug in review step refactor that caused schema-based display settings for individual property values to be discarded. +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0 + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-react@0.4.10 + - @backstage/plugin-scaffolder-common@1.2.5 + +## 1.11.0-next.2 + +### Patch Changes + +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## 1.11.0-next.1 + +### Patch Changes + +- 04f717a8e1: `scaffolder/next`: bump `react-jsonschema-form` libraries to `v5-stable` +- 346d6b6630: Upgrade `@rjsf` version 5 dependencies to `beta.18` +- 0f0da2f256: Prefer schema ordering of template properties during review content generation. +- 38992bdbaf: Fixed bug in review step refactor that caused schema-based display settings for individual property values to be discarded. +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + +## 1.11.0-next.0 + +### Minor Changes + +- d6bb12a2e7: - **Deprecation** - Deprecated the following exports, please import them directly from `@backstage/plugin-scaffolder-react` instead + + ``` + createScaffolderLayout + ScaffolderLayouts + LayoutOptions + LayoutTemplate + ``` + +- 8c2966536b: Embed scaffolder workflow in other components + +### Patch Changes + +- cbab8ac107: lock versions of `@rjsf/*-beta` packages +- d2ddde2108: Add `ScaffolderLayouts` to `NextScaffolderPage` +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-scaffolder-common@1.2.5-next.0 + - @backstage/integration-react@1.1.9 + ## 1.10.0 ### Minor Changes diff --git a/plugins/scaffolder/alpha-api-report.md b/plugins/scaffolder/alpha-api-report.md new file mode 100644 index 0000000000..bf37acafc3 --- /dev/null +++ b/plugins/scaffolder/alpha-api-report.md @@ -0,0 +1,70 @@ +## API Report File for "@backstage/plugin-scaffolder" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="react" /> + +import { Entity } from '@backstage/catalog-model'; +import { FormProps as FormProps_2 } from '@backstage/plugin-scaffolder-react/alpha'; +import type { FormProps as FormProps_3 } from '@rjsf/core-v5'; +import { PathParams } from '@backstage/core-plugin-api'; +import { PropsWithChildren } from 'react'; +import { default as React_2 } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; +import { SubRouteRef } from '@backstage/core-plugin-api'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; + +// @alpha @deprecated +export type FormProps = Pick< + FormProps_3, + 'transformErrors' | 'noHtml5Validate' +>; + +// @alpha (undocumented) +export const nextRouteRef: RouteRef<undefined>; + +// @alpha +export type NextRouterProps = { + components?: { + TemplateCardComponent?: React_2.ComponentType<{ + template: TemplateEntityV1beta3; + }>; + TaskPageComponent?: React_2.ComponentType<{}>; + TemplateOutputsComponent?: React_2.ComponentType<{ + output?: ScaffolderTaskOutput; + }>; + }; + groups?: TemplateGroupFilter[]; + FormProps?: FormProps_2; + contextMenu?: { + editor?: boolean; + actions?: boolean; + tasks?: boolean; + }; +}; + +// @alpha +export const NextScaffolderPage: ( + props: PropsWithChildren<NextRouterProps>, +) => JSX.Element; + +// @alpha (undocumented) +export const nextScaffolderTaskRouteRef: SubRouteRef< + PathParams<'/tasks/:taskId'> +>; + +// @alpha (undocumented) +export const nextSelectedTemplateRouteRef: SubRouteRef< + PathParams<'/templates/:namespace/:templateName'> +>; + +// @alpha (undocumented) +export type TemplateGroupFilter = { + title?: React_2.ReactNode; + filter: (entity: Entity) => boolean; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 82a8dfd8d3..43cc657a03 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -10,26 +10,25 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { createScaffolderFieldExtension as createScaffolderFieldExtension_2 } from '@backstage/plugin-scaffolder-react'; +import { createScaffolderLayout as createScaffolderLayout_2 } from '@backstage/plugin-scaffolder-react'; import { CustomFieldExtensionSchema as CustomFieldExtensionSchema_2 } from '@backstage/plugin-scaffolder-react'; import { CustomFieldValidator as CustomFieldValidator_2 } from '@backstage/plugin-scaffolder-react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } 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 { FieldExtensionComponent as FieldExtensionComponent_2 } from '@backstage/plugin-scaffolder-react'; import { FieldExtensionComponentProps as FieldExtensionComponentProps_2 } from '@backstage/plugin-scaffolder-react'; import { FieldExtensionOptions as FieldExtensionOptions_2 } from '@backstage/plugin-scaffolder-react'; import { FieldValidation } from '@rjsf/core'; -import type { FormProps as FormProps_2 } from '@rjsf/core'; -import type { FormProps as FormProps_3 } from '@rjsf/core-v5'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; +import { LayoutOptions as LayoutOptions_2 } from '@backstage/plugin-scaffolder-react'; +import { LayoutTemplate as LayoutTemplate_2 } from '@backstage/plugin-scaffolder-react'; import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-scaffolder-react'; import { LogEvent as LogEvent_2 } from '@backstage/plugin-scaffolder-react'; import { Observable } from '@backstage/types'; import { PathParams } from '@backstage/core-plugin-api'; -import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -56,10 +55,8 @@ import { z } from 'zod'; // @public @deprecated (undocumented) export const createScaffolderFieldExtension: typeof createScaffolderFieldExtension_2; -// @public -export function createScaffolderLayout<TInputProps = unknown>( - options: LayoutOptions, -): Extension<LayoutComponent<TInputProps>>; +// @public @deprecated (undocumented) +export const createScaffolderLayout: typeof createScaffolderLayout_2; // @public @deprecated (undocumented) export type CustomFieldExtensionSchema = CustomFieldExtensionSchema_2; @@ -155,25 +152,11 @@ export interface FieldSchema<TReturn, TUiOptions> { readonly uiOptionsType: TUiOptions; } -// @alpha @deprecated -export type FormProps = Pick< - FormProps_3, - 'transformErrors' | 'noHtml5Validate' ->; +// @public @deprecated (undocumented) +export type LayoutOptions = LayoutOptions_2; -// @public -export type LayoutComponent<_TInputProps> = () => null; - -// @public -export interface LayoutOptions<P = any> { - // (undocumented) - component: LayoutTemplate<P>; - // (undocumented) - name: string; -} - -// @public -export type LayoutTemplate<T = any> = FormProps_2<T>['ObjectFieldTemplate']; +// @public @deprecated (undocumented) +export type LayoutTemplate = LayoutTemplate_2; // @public @deprecated (undocumented) export type ListActionsResponse = ListActionsResponse_2; @@ -195,36 +178,6 @@ export function makeFieldSchemaFromZod< : never >; -// @alpha (undocumented) -export const nextRouteRef: RouteRef<undefined>; - -// @alpha -export type NextRouterProps = { - components?: { - TemplateCardComponent?: React_2.ComponentType<{ - template: TemplateEntityV1beta3; - }>; - TaskPageComponent?: React_2.ComponentType<{}>; - }; - groups?: TemplateGroupFilter[]; - FormProps?: FormProps; -}; - -// @alpha -export const NextScaffolderPage: ( - props: PropsWithChildren<NextRouterProps>, -) => JSX.Element; - -// @alpha (undocumented) -export const nextScaffolderTaskRouteRef: SubRouteRef< - PathParams<'/tasks/:taskId'> ->; - -// @alpha (undocumented) -export const nextSelectedTemplateRouteRef: SubRouteRef< - PathParams<'/templates/:namespace/:templateName'> ->; - // @public export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2< string, @@ -453,8 +406,8 @@ export type ScaffolderGetIntegrationsListOptions = export type ScaffolderGetIntegrationsListResponse = ScaffolderGetIntegrationsListResponse_2; -// @public -export const ScaffolderLayouts: React.ComponentType; +// @public @deprecated (undocumented) +export const ScaffolderLayouts: ComponentType<{}>; // @public @deprecated (undocumented) export type ScaffolderOutputlink = ScaffolderOutputLink; @@ -514,12 +467,6 @@ export type TaskPageProps = { loadingText?: string; }; -// @alpha (undocumented) -export type TemplateGroupFilter = { - title?: React_2.ReactNode; - filter: (entity: Entity) => boolean; -}; - // @public @deprecated (undocumented) export type TemplateParameterSchema = TemplateParameterSchema_2; diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index 83496c719d..68c59eec2d 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -24,7 +24,7 @@ import { } from '@backstage/plugin-catalog-react'; import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; -import { ScaffolderPage } from '../src/plugin'; +import { NextScaffolderPage, ScaffolderPage } from '../src/plugin'; import { discoveryApiRef, fetchApiRef, @@ -68,4 +68,45 @@ createDevApp() title: 'Create', element: <ScaffolderPage />, }) + .addPage({ + path: '/next-create', + title: 'Create (next)', + element: <NextScaffolderPage />, + }) + .addPage({ + path: '/create-groups', + title: 'Groups', + element: ( + <ScaffolderPage + groups={[ + { + filter: e => e.metadata.tags?.includes('techdocs') || false, + title: 'Techdocs', + }, + { + filter: e => e.metadata.tags?.includes('react') || false, + title: 'React', + }, + ]} + /> + ), + }) + .addPage({ + path: '/next-create-groups', + title: 'Groups (next)', + element: ( + <NextScaffolderPage + groups={[ + { + filter: e => e.metadata.tags?.includes('techdocs') || false, + title: 'Techdocs', + }, + { + filter: e => e.metadata.tags?.includes('react') || false, + title: 'React', + }, + ]} + /> + ), + }) .render(); diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 97ee629cfa..8a0c1f8e26 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,15 +1,27 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "1.10.0", + "version": "1.11.1-next.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", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "frontend-plugin" @@ -24,7 +36,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "start": "backstage-cli package start", "lint": "backstage-cli package lint", "test": "backstage-cli package test", @@ -56,9 +68,9 @@ "@material-ui/lab": "4.0.0-alpha.57", "@react-hookz/web": "^20.0.0", "@rjsf/core": "^3.2.1", - "@rjsf/core-v5": "npm:@rjsf/core@5.0.0-beta.16", "@rjsf/material-ui": "^3.2.1", - "@rjsf/utils": "5.0.0-beta.16", + "@rjsf/utils": "5.1.0", + "@rjsf/validator-ajv8": "5.1.0", "@uiw/react-codemirror": "^4.9.3", "classnames": "^2.2.6", "git-url-parse": "^13.0.0", @@ -70,7 +82,6 @@ "luxon": "^3.0.0", "qs": "^6.9.4", "react-use": "^17.2.4", - "use-immer": "^0.8.0", "yaml": "^2.0.0", "zen-observable": "^0.10.0", "zod": "~3.18.0", @@ -96,10 +107,9 @@ "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", "event-source-polyfill": "1.0.25", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ - "dist", - "alpha" + "dist" ] } diff --git a/plugins/scaffolder/src/alpha.ts b/plugins/scaffolder/src/alpha.ts new file mode 100644 index 0000000000..a460b0d7cc --- /dev/null +++ b/plugins/scaffolder/src/alpha.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { NextScaffolderPage } from './plugin'; +export { + nextRouteRef, + nextScaffolderTaskRouteRef, + nextSelectedTemplateRouteRef, + type TemplateGroupFilter, + type NextRouterProps, + type FormProps, +} from './next'; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 3a1196bc0e..0924c4f2e6 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -35,11 +35,11 @@ import React, { ComponentType, useState } from 'react'; import { transformSchemaToProps } from './schema'; import cloneDeep from 'lodash/cloneDeep'; import * as fieldOverrides from './FieldOverrides'; -import { LayoutOptions } from '../../layouts'; import { ReviewStepProps } from '../types'; import { ReviewStep } from './ReviewStep'; -import { extractSchemaFromStep } from '@backstage/plugin-scaffolder-react'; +import { extractSchemaFromStep } from '@backstage/plugin-scaffolder-react/alpha'; import { selectedTemplateRouteRef } from '../../routes'; +import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; const Form = withTheme(MuiTheme); diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx index d78d726869..112b96bdf1 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx @@ -25,36 +25,38 @@ export function getReviewData( uiSchemas: UiSchema[], ) { const reviewData: Record<string, any> = {}; - for (const key in formData) { - if (formData.hasOwnProperty(key)) { - const uiSchema = uiSchemas.find(us => us.name === key); + const orderedReviewProperties = new Set( + uiSchemas.map(us => us.name).concat(Object.getOwnPropertyNames(formData)), + ); - if (!uiSchema) { - reviewData[key] = formData[key]; - continue; - } + for (const key of orderedReviewProperties) { + const uiSchema = uiSchemas.find(us => us.name === key); - if (uiSchema['ui:widget'] === 'password') { - reviewData[key] = '******'; - continue; - } - - if (!uiSchema['ui:backstage'] || !uiSchema['ui:backstage'].review) { - reviewData[key] = formData[key]; - continue; - } - - const review = uiSchema['ui:backstage'].review as JsonObject; - if (review.mask) { - reviewData[key] = review.mask; - continue; - } - - if (!review.show) { - continue; - } + if (!uiSchema) { reviewData[key] = formData[key]; + continue; } + + if (uiSchema['ui:widget'] === 'password') { + reviewData[key] = '******'; + continue; + } + + if (!uiSchema['ui:backstage'] || !uiSchema['ui:backstage'].review) { + reviewData[key] = formData[key]; + continue; + } + + const review = uiSchema['ui:backstage'].review as JsonObject; + if (review.mask) { + reviewData[key] = review.mask; + continue; + } + + if (!review.show) { + continue; + } + reviewData[key] = formData[key]; } return reviewData; @@ -98,7 +100,12 @@ export const ReviewStep = (props: ReviewStepProps) => { <Typography variant="h6">Review and create</Typography> <StructuredMetadataTable dense - metadata={getReviewData(formData, getUiSchemasFromSteps(steps))} + metadata={getReviewData( + formData, + getUiSchemasFromSteps( + steps.map(({ mergedSchema }) => ({ schema: mergedSchema })), + ), + )} /> <Box mb={4} /> <Button onClick={handleBack} disabled={disableButtons}> diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts index 5d74b0fc81..c3c90ae86a 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts @@ -16,7 +16,7 @@ import { JsonObject } from '@backstage/types'; import { FormProps, UiSchema } from '@rjsf/core'; -import { LayoutOptions } from '../../layouts'; +import type { LayoutOptions } from '@backstage/plugin-scaffolder-react'; function isObject(value: unknown): value is JsonObject { return typeof value === 'object' && value !== null && !Array.isArray(value); @@ -120,7 +120,8 @@ export function transformSchemaToProps( )?.component; if (Layout) { - uiSchema['ui:ObjectFieldTemplate'] = Layout; + uiSchema['ui:ObjectFieldTemplate'] = + Layout as unknown as FormProps<any>['ObjectFieldTemplate']; } } diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 6a2da406e0..ccc2bd283b 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -24,18 +24,14 @@ import { TaskPage } from './TaskPage'; import { ActionsPage } from './ActionsPage'; import { TemplateEditorPage } from './TemplateEditorPage'; import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../extensions/default'; -import { - useElementFilter, - useRouteRef, - useRouteRefParams, -} from '@backstage/core-plugin-api'; +import { useRouteRef, useRouteRefParams } from '@backstage/core-plugin-api'; import { FieldExtensionOptions, SecretsContextProvider, useCustomFieldExtensions, + useCustomLayouts, } from '@backstage/plugin-scaffolder-react'; import { ListTasksPage } from './ListTasksPage'; -import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts'; import { ReviewStepProps } from './types'; import { actionsRouteRef, @@ -104,16 +100,7 @@ export const Router = (props: RouterProps) => { ), ] as FieldExtensionOptions[]; - // todo(blam): this should also be moved to a hook in -react - const customLayouts = useElementFilter(outlet, elements => - elements - .selectByComponentData({ - key: LAYOUTS_WRAPPER_KEY, - }) - .findComponentData<LayoutOptions>({ - key: LAYOUTS_KEY, - }), - ); + const customLayouts = useCustomLayouts(outlet); /** * This component can be deleted once the older routes have been deprecated. diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 50221f3a83..e153c2e232 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -36,7 +36,7 @@ import { import React, { ComponentType } from 'react'; import { TemplateList } from '../TemplateList'; import { TemplateTypePicker } from '../TemplateTypePicker'; -import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; import { usePermission } from '@backstage/plugin-permission-react'; import { ScaffolderPageContextMenu } from './ScaffolderPageContextMenu'; import { registerComponentRouteRef } from '../../routes'; diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 393bd495bf..a45f0cf413 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -50,8 +50,8 @@ import useInterval from 'react-use/lib/useInterval'; import { ScaffolderTaskStatus, ScaffolderTaskOutput, + useTaskEventStream, } from '@backstage/plugin-scaffolder-react'; -import { useTaskEventStream } from '../hooks/useEventStream'; import { TaskErrors } from './TaskErrors'; import { TaskPageLinks } from './TaskPageLinks'; import { diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 984e51f3b5..e557f4d165 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DEFAULT_NAMESPACE, Entity, @@ -22,9 +23,9 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { - Button, ItemCardHeader, Link, + LinkButton, MarkdownContent, } from '@backstage/core-components'; import { @@ -291,13 +292,13 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { </Tooltip> ))} </div> - <Button + <LinkButton color="primary" to={href} aria-label={`Choose ${templateProps.title}`} > Choose - </Button> + </LinkButton> </CardActions> </Card> ); diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx index dc67f687bb..e07434aff7 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx @@ -15,8 +15,10 @@ */ import { makeStyles } from '@material-ui/core'; import React, { useState } from 'react'; -import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; -import type { LayoutOptions } from '../../layouts'; +import type { + FieldExtensionOptions, + LayoutOptions, +} from '@backstage/plugin-scaffolder-react'; import { TemplateDirectoryAccess } from '../../lib/filesystem'; import { DirectoryEditorProvider } from './DirectoryEditorContext'; import { DryRunProvider } from './DryRunContext'; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx index c62112a89c..315028219b 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx @@ -19,11 +19,11 @@ import { makeStyles } from '@material-ui/core/styles'; import React, { Component, ReactNode, useMemo, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import yaml from 'yaml'; -import { +import type { FieldExtensionOptions, + LayoutOptions, TemplateParameterSchema, } from '@backstage/plugin-scaffolder-react'; -import { LayoutOptions } from '../../layouts'; import { MultistepJsonForm } from '../MultistepJsonForm'; import { createValidator } from '../TemplatePage'; import { useDirectoryEditor } from './DirectoryEditorContext'; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx index 20691a8e85..ebb763e145 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx @@ -23,8 +23,10 @@ import { CustomFieldExplorer } from './CustomFieldExplorer'; import { TemplateEditorIntro } from './TemplateEditorIntro'; import { TemplateEditor } from './TemplateEditor'; import { TemplateFormPreviewer } from './TemplateFormPreviewer'; -import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; -import type { LayoutOptions } from '../../layouts'; +import { + type FieldExtensionOptions, + type LayoutOptions, +} from '@backstage/plugin-scaffolder-react'; type Selection = | { diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx index 884305bf64..a7e3c6a697 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx @@ -32,8 +32,10 @@ import CloseIcon from '@material-ui/icons/Close'; import React, { useCallback, useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import yaml from 'yaml'; -import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; -import { LayoutOptions } from '../../layouts'; +import { + type FieldExtensionOptions, + type LayoutOptions, +} from '@backstage/plugin-scaffolder-react'; import { TemplateEditorForm } from './TemplateEditorForm'; import { TemplateEditorTextArea } from './TemplateEditorTextArea'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 0f484ff404..c615649f93 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -20,7 +20,8 @@ import React, { ComponentType, useCallback, useState } from 'react'; import { Navigate, useNavigate } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; import { - FieldExtensionOptions, + type FieldExtensionOptions, + type LayoutOptions, scaffolderApiRef, useTemplateSecrets, } from '@backstage/plugin-scaffolder-react'; @@ -37,7 +38,6 @@ import { useRouteRefParams, } from '@backstage/core-plugin-api'; import { stringifyEntityRef } from '@backstage/catalog-model'; -import { LayoutOptions } from '../../layouts'; import { ReviewStepProps } from '../types'; import { rootRouteRef, diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts index 177697eda0..f5be77811a 100644 --- a/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts @@ -17,14 +17,32 @@ import { createValidator } from './createValidator'; import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; import { ApiHolder } from '@backstage/core-plugin-api'; -import { FormValidation } from '@rjsf/core'; +import { FieldValidation, FormValidation } from '@rjsf/core'; describe('createValidator', () => { const validators: Record<string, undefined | CustomFieldValidator<unknown>> = { - CustomPicker: (value, validation, _context) => { + CustomPicker: ( + value: unknown, + fieldValidation: FieldValidation, + _context: { apiHolder: ApiHolder }, + ) => { if (!value || !(value as { value?: unknown }).value) { - validation.addError('Error !'); + fieldValidation.addError('Error !'); + } + }, + TagPicker: ( + values: unknown, + fieldValidation: FieldValidation, + _context: { apiHolder: ApiHolder }, + ) => { + const input = values as string[]; + for (const item of input) { + if (!/^[a-z0-9-]+$/.test(item)) { + fieldValidation.addError( + 'A tag name can only contain lowercase letters, numeric characters or dashes', + ); + } } }, }; @@ -39,7 +57,7 @@ describe('createValidator', () => { apiHolder: apiHolderMock, }; - it('Should call validator for object property from a custom field extension', () => { + it('should call validator for object property from a custom field extension', () => { /* GIVEN */ const rootSchema = { title: 'Title', @@ -70,4 +88,39 @@ describe('createValidator', () => { expect(result).not.toBeNull(); expect(result.p1.addError).toHaveBeenCalledTimes(1); }); + + it('should call validator for array property from a custom field extension', () => { + /* GIVEN */ + const rootSchema = { + title: 'My form', + properties: { + tags: { + title: 'Tags', + type: 'array', + items: { + type: 'string', + 'ui:field': 'TagPicker', + }, + }, + }, + }; + const validator = createValidator(rootSchema, validators, context); + + const formData = { + tags: ['invalid-tag$$'], + }; + const errors = { + addError: jest.fn(), + tags: { + addError: jest.fn(), + } as unknown as FormValidation, + } as unknown as FormValidation; + + /* WHEN */ + const result = validator(formData, errors); + + /* THEN */ + expect(result).not.toBeNull(); + expect(result.tags.addError).toHaveBeenCalledTimes(1); + }); }); diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts index 4a64481fc3..123a7e800e 100644 --- a/plugins/scaffolder/src/components/TemplatePage/createValidator.ts +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts @@ -23,6 +23,10 @@ function isObject(obj: unknown): obj is JsonObject { return typeof obj === 'object' && obj !== null && !Array.isArray(obj); } +function isArray(obj: unknown): obj is JsonObject { + return typeof obj === 'object' && obj !== null && Array.isArray(obj); +} + export const createValidator = ( rootSchema: JsonObject, validators: Record<string, undefined | CustomFieldValidator<unknown>>, @@ -46,8 +50,8 @@ export const createValidator = ( for (const [key, propData] of Object.entries(formData)) { const propValidation = errors[key]; + const propSchemaProps = schemaProps[key]; if (isObject(propData)) { - const propSchemaProps = schemaProps[key]; if (isObject(propSchemaProps)) { validate( propSchemaProps, @@ -55,10 +59,24 @@ export const createValidator = ( propValidation as FormValidation, ); } + } else if (isArray(propData)) { + if (isObject(propSchemaProps)) { + const { items } = propSchemaProps; + if (isObject(items)) { + const fieldName = items['ui:field'] as string; + if (fieldName && typeof validators[fieldName] === 'function') { + validators[fieldName]!( + propData as JsonObject[], + propValidation, + context, + ); + } + } + } } else { - const propSchema = schemaProps[key]; const fieldName = - isObject(propSchema) && (propSchema['ui:field'] as string); + isObject(propSchemaProps) && + (propSchemaProps['ui:field'] as string); if (fieldName && typeof validators[fieldName] === 'function') { validators[fieldName]!( propData as JsonValue, diff --git a/plugins/scaffolder/src/deprecated.ts b/plugins/scaffolder/src/deprecated.ts index 201f7ac7d7..f08a83c594 100644 --- a/plugins/scaffolder/src/deprecated.ts +++ b/plugins/scaffolder/src/deprecated.ts @@ -25,6 +25,10 @@ import { ScaffolderFieldExtensions as ScaffolderFieldExtensionsTemp, useTemplateSecrets as useTemplateSecretsTemp, scaffolderApiRef as scaffolderApiRefTemp, + createScaffolderLayout as createScaffolderLayoutTemp, + ScaffolderLayouts as ScaffolderLayoutsTemp, + type LayoutOptions as LayoutOptionsTemp, + type LayoutTemplate as LayoutTemplateTemp, type ScaffolderApi as ScaffolderApiTemp, type ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecretsTemp, type TemplateParameterSchema as TemplateParameterSchemaTemp, @@ -188,3 +192,23 @@ export type ScaffolderTaskOutput = ScaffolderTaskOutputTemp; * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTaskStatus} instead as this has now been moved. */ export type ScaffolderTaskStatus = ScaffolderTaskStatusTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#createScaffolderLayout} instead as this has now been moved. + */ +export const createScaffolderLayout = createScaffolderLayoutTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderLayouts} instead as this has now been moved. + */ +export const ScaffolderLayouts = ScaffolderLayoutsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#LayoutTemplate} instead as this has now been moved. + */ +export type LayoutTemplate = LayoutTemplateTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#LayoutOptions} instead as this has now been moved. + */ +export type LayoutOptions = LayoutOptionsTemp; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 0e2b293982..bc56629a2c 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -21,14 +21,6 @@ */ export { ScaffolderClient } from './api'; -export { - createScaffolderLayout, - ScaffolderLayouts, - type LayoutOptions, - type LayoutTemplate, - type LayoutComponent, -} from './layouts'; - export { EntityPickerFieldExtension, EntityNamePickerFieldExtension, @@ -42,14 +34,3 @@ export { export * from './components'; export * from './deprecated'; - -/** next exports */ -export { NextScaffolderPage } from './plugin'; -export { - nextRouteRef, - nextScaffolderTaskRouteRef, - nextSelectedTemplateRouteRef, - type TemplateGroupFilter, - type NextRouterProps, - type FormProps, -} from './next'; diff --git a/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx new file mode 100644 index 0000000000..d4c771a9f8 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { BackstageTheme } from '@backstage/theme'; +import { + IconButton, + ListItemIcon, + ListItemText, + makeStyles, + MenuItem, + MenuList, + Popover, +} from '@material-ui/core'; +import Retry from '@material-ui/icons/Repeat'; +import Toc from '@material-ui/icons/Toc'; +import MoreVert from '@material-ui/icons/MoreVert'; +import React, { useState } from 'react'; + +type ContextMenuProps = { + logsVisible?: boolean; + onToggleLogs?: (state: boolean) => void; + onStartOver?: () => void; +}; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + button: { + color: theme.palette.common.white, + }, +})); + +export const ContextMenu = (props: ContextMenuProps) => { + const { logsVisible, onToggleLogs, onStartOver } = props; + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>(); + + return ( + <> + <IconButton + aria-label="more" + aria-controls="long-menu" + aria-haspopup="true" + onClick={(event: React.SyntheticEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget); + }} + data-testid="menu-button" + color="inherit" + className={classes.button} + > + <MoreVert /> + </IconButton> + <Popover + open={Boolean(anchorEl)} + onClose={() => setAnchorEl(undefined)} + anchorEl={anchorEl} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + > + <MenuList> + <MenuItem onClick={() => onToggleLogs?.(!logsVisible)}> + <ListItemIcon> + <Toc fontSize="small" /> + </ListItemIcon> + <ListItemText primary={logsVisible ? 'Hide Logs' : 'Show Logs'} /> + </MenuItem> + <MenuItem onClick={onStartOver}> + <ListItemIcon> + <Retry fontSize="small" /> + </ListItemIcon> + <ListItemText primary="Start Over" /> + </MenuItem> + </MenuList> + </Popover> + </> + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx new file mode 100644 index 0000000000..84730429dd --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx @@ -0,0 +1,160 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useEffect, useMemo, useState, useCallback } from 'react'; +import { Page, Header, Content, ErrorPanel } from '@backstage/core-components'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Box, makeStyles, Paper } from '@material-ui/core'; +import { + TaskSteps, + TaskBorder, + TaskLogStream, + ScaffolderTaskOutput, + useTaskEventStream, +} from '@backstage/plugin-scaffolder-react'; +import { nextSelectedTemplateRouteRef } from '../routes'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import qs from 'qs'; +import { ContextMenu } from './ContextMenu'; +import { DefaultTemplateOutputs } from '@backstage/plugin-scaffolder-react/alpha'; + +const useStyles = makeStyles({ + contentWrapper: { + display: 'flex', + flexDirection: 'column', + }, +}); + +export const OngoingTask = (props: { + TemplateOutputsComponent?: React.ComponentType<{ + output?: ScaffolderTaskOutput; + }>; +}) => { + // todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. + const { taskId } = useParams(); + const templateRouteRef = useRouteRef(nextSelectedTemplateRouteRef); + const navigate = useNavigate(); + const taskStream = useTaskEventStream(taskId!); + const classes = useStyles(); + const steps = useMemo( + () => + taskStream.task?.spec.steps.map(step => ({ + ...step, + ...taskStream?.steps?.[step.id], + })) ?? [], + [taskStream], + ); + + const [logsVisible, setLogVisibleState] = useState(false); + + useEffect(() => { + if (taskStream.error) { + setLogVisibleState(true); + } + }, [taskStream.error]); + + const activeStep = useMemo(() => { + for (let i = steps.length - 1; i >= 0; i--) { + if (steps[i].status !== 'open') { + return i; + } + } + + return 0; + }, [steps]); + + const startOver = useCallback(() => { + const { namespace, name } = + taskStream.task?.spec.templateInfo?.entity?.metadata ?? {}; + + const formData = taskStream.task?.spec.parameters ?? {}; + + if (!namespace || !name) { + return; + } + + navigate({ + pathname: templateRouteRef({ + namespace, + templateName: name, + }), + search: `?${qs.stringify({ formData: JSON.stringify(formData) })}`, + }); + }, [ + navigate, + taskStream.task?.spec.parameters, + taskStream.task?.spec.templateInfo?.entity?.metadata, + templateRouteRef, + ]); + + const Outputs = props.TemplateOutputsComponent ?? DefaultTemplateOutputs; + + const templateName = + taskStream.task?.spec.templateInfo?.entity?.metadata.name; + + return ( + <Page themeId="website"> + <Header + pageTitleOverride={`Run of ${templateName}`} + title={ + <div> + Run of <code>{templateName}</code> + </div> + } + subtitle={`Task ${taskId}`} + > + <ContextMenu + onToggleLogs={setLogVisibleState} + onStartOver={startOver} + logsVisible={logsVisible} + /> + </Header> + <Content className={classes.contentWrapper}> + {taskStream.error ? ( + <Box paddingBottom={2}> + <ErrorPanel + error={taskStream.error} + title={taskStream.error.message} + /> + </Box> + ) : null} + + <Box paddingBottom={2}> + <Paper style={{ position: 'relative', overflow: 'hidden' }}> + <TaskBorder + isComplete={taskStream.completed} + isError={Boolean(taskStream.error)} + /> + <Box padding={2}> + <TaskSteps steps={steps} activeStep={activeStep} /> + </Box> + </Paper> + </Box> + + <Outputs output={taskStream.output} /> + + {logsVisible ? ( + <Box paddingBottom={2} height="100%"> + <Paper style={{ height: '100%' }}> + <Box padding={2} height="100%"> + <TaskLogStream logs={taskStream.stepLogs} /> + </Box> + </Paper> + </Box> + ) : null} + </Content> + </Page> + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/index.ts b/plugins/scaffolder/src/next/OngoingTask/index.ts new file mode 100644 index 0000000000..f0c7e2a6cc --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { OngoingTask } from './OngoingTask'; diff --git a/plugins/scaffolder/src/next/Router/Router.test.tsx b/plugins/scaffolder/src/next/Router/Router.test.tsx index 4e5535be6c..9565572a2f 100644 --- a/plugins/scaffolder/src/next/Router/Router.test.tsx +++ b/plugins/scaffolder/src/next/Router/Router.test.tsx @@ -23,6 +23,10 @@ import { ScaffolderFieldExtensions, } from '@backstage/plugin-scaffolder-react'; import { scaffolderPlugin } from '../../plugin'; +import { + createScaffolderLayout, + ScaffolderLayouts, +} from '@backstage/plugin-scaffolder-react'; jest.mock('../TemplateListPage', () => ({ TemplateListPage: jest.fn(() => null), @@ -106,5 +110,36 @@ describe('Router', () => { ]), ); }); + + it('should extract the layouts and pass them through', async () => { + const mockLayoutComponent = () => null; + const Layout = scaffolderPlugin.provide( + createScaffolderLayout({ + name: 'layout', + component: mockLayoutComponent, + }), + ); + + await renderInTestApp( + <Router> + <ScaffolderLayouts> + <Layout /> + </ScaffolderLayouts> + </Router>, + { routeEntries: ['/templates/default/foo'] }, + ); + + const mock = TemplateWizardPage as jest.Mock; + const [{ layouts }] = mock.mock.calls[0]; + + expect(layouts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'layout', + component: mockLayoutComponent, + }), + ]), + ); + }); }); }); diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index f91e56bba7..5e199cae9b 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -19,15 +19,31 @@ import { TemplateListPage } from '../TemplateListPage'; import { TemplateWizardPage } from '../TemplateWizardPage'; import { NextFieldExtensionOptions, + FormProps, +} from '@backstage/plugin-scaffolder-react/alpha'; +import { + ScaffolderTaskOutput, SecretsContextProvider, useCustomFieldExtensions, + useCustomLayouts, } from '@backstage/plugin-scaffolder-react'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups'; import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default'; -import { type FormProps } from '../types'; -import { nextSelectedTemplateRouteRef } from '../routes'; + +import { + nextActionsRouteRef, + nextEditRouteRef, + nextScaffolderListTaskRouteRef, + nextScaffolderTaskRouteRef, + nextSelectedTemplateRouteRef, +} from '../routes'; +import { ErrorPage } from '@backstage/core-components'; +import { OngoingTask } from '../OngoingTask'; +import { ActionsPage } from '../../components/ActionsPage'; +import { ListTasksPage } from '../../components/ListTasksPage'; +import { TemplateEditorPage } from '../TemplateEditorPage'; /** * The Props for the Scaffolder Router @@ -40,9 +56,21 @@ export type NextRouterProps = { template: TemplateEntityV1beta3; }>; TaskPageComponent?: React.ComponentType<{}>; + TemplateOutputsComponent?: React.ComponentType<{ + output?: ScaffolderTaskOutput; + }>; }; groups?: TemplateGroupFilter[]; + // todo(blam): rename this to formProps FormProps?: FormProps; + contextMenu?: { + /** Whether to show a link to the template editor */ + editor?: boolean; + /** Whether to show a link to the actions documentation */ + actions?: boolean; + /** Whether to show a link to the tasks page */ + tasks?: boolean; + }; }; /** @@ -51,10 +79,17 @@ export type NextRouterProps = { * @alpha */ export const Router = (props: PropsWithChildren<NextRouterProps>) => { - const { components: { TemplateCardComponent } = {} } = props; + const { + components: { + TemplateCardComponent, + TemplateOutputsComponent, + TaskPageComponent = OngoingTask, + } = {}, + } = props; const outlet = useOutlet() || props.children; const customFieldExtensions = useCustomFieldExtensions<NextFieldExtensionOptions>(outlet); + const fieldExtensions = [ ...customFieldExtensions, ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( @@ -65,6 +100,8 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => { ), ] as NextFieldExtensionOptions[]; + const customLayouts = useCustomLayouts(outlet); + return ( <Routes> <Route @@ -72,6 +109,7 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => { element={ <TemplateListPage TemplateCardComponent={TemplateCardComponent} + contextMenu={props.contextMenu} groups={props.groups} /> } @@ -82,11 +120,41 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => { <SecretsContextProvider> <TemplateWizardPage customFieldExtensions={fieldExtensions} + layouts={customLayouts} FormProps={props.FormProps} /> </SecretsContextProvider> } /> + <Route + path={nextScaffolderTaskRouteRef.path} + element={ + <TaskPageComponent + TemplateOutputsComponent={TemplateOutputsComponent} + /> + } + /> + <Route + path={nextEditRouteRef.path} + element={ + <SecretsContextProvider> + <TemplateEditorPage + customFieldExtensions={fieldExtensions} + layouts={customLayouts} + /> + </SecretsContextProvider> + } + /> + + <Route path={nextActionsRouteRef.path} element={<ActionsPage />} /> + <Route + path={nextScaffolderListTaskRouteRef.path} + element={<ListTasksPage />} + /> + <Route + path="*" + element={<ErrorPage status="404" statusMessage="Page not found" />} + /> </Routes> ); }; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/CustomFieldExplorer.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/CustomFieldExplorer.tsx new file mode 100644 index 0000000000..823d5b164a --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/CustomFieldExplorer.tsx @@ -0,0 +1,196 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { StreamLanguage } from '@codemirror/language'; +import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; +import { + Button, + Card, + CardContent, + CardHeader, + FormControl, + IconButton, + InputLabel, + makeStyles, + MenuItem, + Select, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import CodeMirror from '@uiw/react-codemirror'; +import React, { useCallback, useMemo, useState } from 'react'; +import yaml from 'yaml'; +import { + NextFieldExtensionOptions, + Form, +} from '@backstage/plugin-scaffolder-react/alpha'; +import { TemplateEditorForm } from './TemplateEditorForm'; +import validator from '@rjsf/validator-ajv8'; + +const useStyles = makeStyles(theme => ({ + root: { + gridArea: 'pageContent', + display: 'grid', + gridTemplateAreas: ` + "controls controls" + "fieldForm preview" + `, + gridTemplateRows: 'auto 1fr', + gridTemplateColumns: '1fr 1fr', + }, + controls: { + gridArea: 'controls', + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + margin: theme.spacing(1), + }, + fieldForm: { + gridArea: 'fieldForm', + }, + preview: { + gridArea: 'preview', + }, +})); + +export const CustomFieldExplorer = ({ + customFieldExtensions = [], + onClose, +}: { + customFieldExtensions?: NextFieldExtensionOptions<any, any>[]; + onClose?: () => void; +}) => { + const classes = useStyles(); + const fieldOptions = customFieldExtensions.filter(field => !!field.schema); + const [selectedField, setSelectedField] = useState(fieldOptions[0]); + const [fieldFormState, setFieldFormState] = useState({}); + const [refreshKey, setRefreshKey] = useState(Date.now()); + const sampleFieldTemplate = useMemo( + () => + yaml.stringify({ + parameters: [ + { + title: `${selectedField.name} Example`, + properties: { + [selectedField.name]: { + type: selectedField.schema?.returnValue?.type, + 'ui:field': selectedField.name, + 'ui:options': fieldFormState, + }, + }, + }, + ], + }), + [fieldFormState, selectedField], + ); + + const fieldComponents = useMemo(() => { + return Object.fromEntries( + customFieldExtensions.map(({ name, component }) => [name, component]), + ); + }, [customFieldExtensions]); + + const handleSelectionChange = useCallback( + selection => { + setSelectedField(selection); + setFieldFormState({}); + }, + [setFieldFormState, setSelectedField], + ); + + const handleFieldConfigChange = useCallback( + state => { + setFieldFormState(state); + // Force TemplateEditorForm to re-render since some fields + // may not be responsive to ui:option changes + setRefreshKey(Date.now()); + }, + [setFieldFormState, setRefreshKey], + ); + + return ( + <main className={classes.root}> + <div className={classes.controls}> + <FormControl variant="outlined" size="small" fullWidth> + <InputLabel id="select-field-label"> + Choose Custom Field Extension + </InputLabel> + <Select + value={selectedField} + label="Choose Custom Field Extension" + labelId="select-field-label" + onChange={e => handleSelectionChange(e.target.value)} + > + {fieldOptions.map((option, idx) => ( + <MenuItem key={idx} value={option as any}> + {option.name} + </MenuItem> + ))} + </Select> + </FormControl> + + <IconButton size="medium" onClick={onClose}> + <CloseIcon /> + </IconButton> + </div> + <div className={classes.fieldForm}> + <Card> + <CardHeader title="Field Options" /> + <CardContent> + <Form + showErrorList={false} + fields={{ ...fieldComponents }} + noHtml5Validate + formData={fieldFormState} + formContext={{ fieldFormState }} + onSubmit={e => handleFieldConfigChange(e.formData)} + validator={validator} + schema={selectedField.schema?.uiOptions || {}} + > + <Button + variant="contained" + color="primary" + type="submit" + disabled={!selectedField.schema?.uiOptions} + > + Apply + </Button> + </Form> + </CardContent> + </Card> + </div> + <div className={classes.preview}> + <Card> + <CardHeader title="Example Template Spec" /> + <CardContent> + <CodeMirror + readOnly + theme="dark" + height="100%" + extensions={[StreamLanguage.define(yamlSupport)]} + value={sampleFieldTemplate} + /> + </CardContent> + </Card> + <TemplateEditorForm + key={refreshKey} + content={sampleFieldTemplate} + contentIsSpec + fieldExtensions={customFieldExtensions} + setErrorText={() => null} + /> + </div> + </main> + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditor.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditor.tsx new file mode 100644 index 0000000000..16067e8095 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditor.tsx @@ -0,0 +1,92 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { makeStyles } from '@material-ui/core'; +import React, { useState } from 'react'; +import type { LayoutOptions } from '@backstage/plugin-scaffolder-react'; +import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; +import { TemplateDirectoryAccess } from '../../lib/filesystem'; +import { DirectoryEditorProvider } from '../../components/TemplateEditorPage/DirectoryEditorContext'; +import { DryRunProvider } from '../../components/TemplateEditorPage/DryRunContext'; +import { DryRunResults } from '../../components/TemplateEditorPage/DryRunResults'; +import { TemplateEditorBrowser } from '../../components/TemplateEditorPage/TemplateEditorBrowser'; +import { TemplateEditorForm } from './TemplateEditorForm'; +import { TemplateEditorTextArea } from '../../components/TemplateEditorPage/TemplateEditorTextArea'; + +const useStyles = makeStyles({ + // Reset and fix sizing to make sure scrolling behaves correctly + root: { + gridArea: 'pageContent', + + display: 'grid', + gridTemplateAreas: ` + "browser editor preview" + "results results results" + `, + gridTemplateColumns: '1fr 3fr 2fr', + gridTemplateRows: '1fr auto', + }, + browser: { + gridArea: 'browser', + overflow: 'auto', + }, + editor: { + gridArea: 'editor', + overflow: 'auto', + }, + preview: { + gridArea: 'preview', + overflow: 'auto', + }, + results: { + gridArea: 'results', + }, +}); + +export const TemplateEditor = (props: { + directory: TemplateDirectoryAccess; + fieldExtensions?: NextFieldExtensionOptions<any, any>[]; + layouts?: LayoutOptions[]; + onClose?: () => void; +}) => { + const classes = useStyles(); + + const [errorText, setErrorText] = useState<string>(); + + return ( + <DirectoryEditorProvider directory={props.directory}> + <DryRunProvider> + <main className={classes.root}> + <section className={classes.browser}> + <TemplateEditorBrowser onClose={props.onClose} /> + </section> + <section className={classes.editor}> + <TemplateEditorTextArea.DirectoryEditor errorText={errorText} /> + </section> + <section className={classes.preview}> + <TemplateEditorForm.DirectoryEditorDryRun + setErrorText={setErrorText} + fieldExtensions={props.fieldExtensions} + layouts={props.layouts} + /> + </section> + <section className={classes.results}> + <DryRunResults /> + </section> + </main> + </DryRunProvider> + </DirectoryEditorProvider> + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorForm.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorForm.tsx new file mode 100644 index 0000000000..898487e200 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorForm.tsx @@ -0,0 +1,235 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useApiHolder } from '@backstage/core-plugin-api'; +import { JsonObject, JsonValue } from '@backstage/types'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { Component, ReactNode, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import yaml from 'yaml'; +import { + LayoutOptions, + TemplateParameterSchema, +} from '@backstage/plugin-scaffolder-react'; +import { + NextFieldExtensionOptions, + Stepper, +} from '@backstage/plugin-scaffolder-react/alpha'; +import { useDryRun } from '../../components/TemplateEditorPage/DryRunContext'; +import { useDirectoryEditor } from '../../components/TemplateEditorPage/DirectoryEditorContext'; + +const useStyles = makeStyles({ + containerWrapper: { + position: 'relative', + width: '100%', + height: '100%', + }, + container: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + overflow: 'auto', + }, +}); + +interface ErrorBoundaryProps { + invalidator: unknown; + setErrorText(errorText: string | undefined): void; + children: ReactNode; +} + +interface ErrorBoundaryState { + shouldRender: boolean; +} + +class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { + state = { + shouldRender: true, + }; + + componentDidUpdate(prevProps: { invalidator: unknown }) { + if (prevProps.invalidator !== this.props.invalidator) { + this.setState({ shouldRender: true }); + } + } + + componentDidCatch(error: Error) { + this.props.setErrorText(error.message); + this.setState({ shouldRender: false }); + } + + render() { + return this.state.shouldRender ? this.props.children : null; + } +} + +interface TemplateEditorFormProps { + content?: string; + /** Setting this to true will cause the content to be parsed as if it is the template entity spec */ + contentIsSpec?: boolean; + setErrorText: (errorText?: string) => void; + onDryRun?: (data: JsonObject) => Promise<void>; + fieldExtensions?: NextFieldExtensionOptions<any, any>[]; + layouts?: LayoutOptions[]; +} + +function isJsonObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** Shows the a template form that is parsed from the provided content */ +export function TemplateEditorForm(props: TemplateEditorFormProps) { + const { + content, + contentIsSpec, + onDryRun, + setErrorText, + fieldExtensions = [], + layouts = [], + } = props; + const classes = useStyles(); + const apiHolder = useApiHolder(); + + const [steps, setSteps] = useState<TemplateParameterSchema['steps']>(); + + useDebounce( + () => { + try { + if (!content) { + setSteps(undefined); + return; + } + const parsed: JsonValue = yaml.parse(content); + + if (!isJsonObject(parsed)) { + setSteps(undefined); + return; + } + + let rootObj = parsed; + if (!contentIsSpec) { + const isTemplate = + String(parsed.kind).toLocaleLowerCase('en-US') === 'template'; + if (!isTemplate) { + setSteps(undefined); + return; + } + + rootObj = isJsonObject(parsed.spec) ? parsed.spec : {}; + } + + const { parameters } = rootObj; + + if (!Array.isArray(parameters)) { + setErrorText('Template parameters must be an array'); + setSteps(undefined); + return; + } + + setErrorText(); + setSteps( + parameters.flatMap(param => + isJsonObject(param) + ? [ + { + title: String(param.title), + schema: param, + }, + ] + : [], + ), + ); + } catch (e) { + setErrorText(e.message); + } + }, + 250, + [contentIsSpec, content, apiHolder], + ); + + if (!steps) { + return null; + } + + return ( + <div className={classes.containerWrapper}> + <div className={classes.container}> + <ErrorBoundary invalidator={steps} setErrorText={setErrorText}> + <Stepper + manifest={{ steps, title: 'Template Editor' }} + extensions={fieldExtensions} + onCreate={async data => { + await onDryRun?.(data); + }} + layouts={layouts} + components={{ createButtonText: onDryRun && 'Try It' }} + /> + </ErrorBoundary> + </div> + </div> + ); +} + +/** A version of the TemplateEditorForm that is connected to the DirectoryEditor and DryRun contexts */ +export function TemplateEditorFormDirectoryEditorDryRun( + props: Pick< + TemplateEditorFormProps, + 'setErrorText' | 'fieldExtensions' | 'layouts' + >, +) { + const { setErrorText, fieldExtensions = [], layouts } = props; + const dryRun = useDryRun(); + + const directoryEditor = useDirectoryEditor(); + const { selectedFile } = directoryEditor; + + const handleDryRun = async (values: JsonObject) => { + if (!selectedFile) { + return; + } + + try { + await dryRun.execute({ + templateContent: selectedFile.content, + values, + files: directoryEditor.files, + }); + setErrorText(); + } catch (e) { + setErrorText(String(e.cause || e)); + throw e; + } + }; + + const content = + selectedFile && selectedFile.path.match(/\.ya?ml$/) + ? selectedFile.content + : undefined; + + return ( + <TemplateEditorForm + onDryRun={handleDryRun} + fieldExtensions={fieldExtensions} + setErrorText={setErrorText} + content={content} + layouts={layouts} + /> + ); +} + +TemplateEditorForm.DirectoryEditorDryRun = + TemplateEditorFormDirectoryEditorDryRun; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx new file mode 100644 index 0000000000..9863e99c73 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 } from 'react'; +import { Content, Header, Page } from '@backstage/core-components'; +import { + TemplateDirectoryAccess, + WebFileSystemAccess, +} from '../../lib/filesystem'; +import { CustomFieldExplorer } from './CustomFieldExplorer'; +import { TemplateEditor } from './TemplateEditor'; +import { TemplateFormPreviewer } from './TemplateFormPreviewer'; +import { type LayoutOptions } from '@backstage/plugin-scaffolder-react'; +import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; +import { TemplateEditorIntro } from '../../components/TemplateEditorPage/TemplateEditorIntro'; + +type Selection = + | { + type: 'local'; + directory: TemplateDirectoryAccess; + } + | { + type: 'form'; + } + | { + type: 'field-explorer'; + }; + +interface TemplateEditorPageProps { + defaultPreviewTemplate?: string; + customFieldExtensions?: NextFieldExtensionOptions<any, any>[]; + layouts?: LayoutOptions[]; +} + +export function TemplateEditorPage(props: TemplateEditorPageProps) { + const [selection, setSelection] = useState<Selection>(); + + let content: JSX.Element | null = null; + if (selection?.type === 'local') { + content = ( + <TemplateEditor + directory={selection.directory} + fieldExtensions={props.customFieldExtensions} + onClose={() => setSelection(undefined)} + layouts={props.layouts} + /> + ); + } else if (selection?.type === 'form') { + content = ( + <TemplateFormPreviewer + defaultPreviewTemplate={props.defaultPreviewTemplate} + customFieldExtensions={props.customFieldExtensions} + onClose={() => setSelection(undefined)} + layouts={props.layouts} + /> + ); + } else if (selection?.type === 'field-explorer') { + content = ( + <CustomFieldExplorer + customFieldExtensions={props.customFieldExtensions} + onClose={() => setSelection(undefined)} + /> + ); + } else { + content = ( + <Content> + <TemplateEditorIntro + onSelect={option => { + if (option === 'local') { + WebFileSystemAccess.requestDirectoryAccess() + .then(directory => setSelection({ type: 'local', directory })) + .catch(() => {}); + } else if (option === 'form') { + setSelection({ type: 'form' }); + } else if (option === 'field-explorer') { + setSelection({ type: 'field-explorer' }); + } + }} + /> + </Content> + ); + } + + return ( + <Page themeId="home"> + <Header + title="Template Editor" + subtitle="Edit, preview, and try out templates and template forms" + /> + {content} + </Page> + ); +} diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx new file mode 100644 index 0000000000..2aa1cc764d --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx @@ -0,0 +1,217 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { alertApiRef, useApi } from '@backstage/core-plugin-api'; +import { + catalogApiRef, + humanizeEntityRef, +} from '@backstage/plugin-catalog-react'; +import { + FormControl, + IconButton, + InputLabel, + LinearProgress, + makeStyles, + MenuItem, + Select, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import React, { useCallback, useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import yaml from 'yaml'; +import { type LayoutOptions } from '@backstage/plugin-scaffolder-react'; +import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; +import { TemplateEditorForm } from './TemplateEditorForm'; +import { TemplateEditorTextArea } from '../../components/TemplateEditorPage/TemplateEditorTextArea'; + +const EXAMPLE_TEMPLATE_PARAMS_YAML = `# Edit the template parameters below to see how they will render in the scaffolder form UI +parameters: + - title: Fill in some steps + required: + - name + properties: + name: + title: Name + type: string + description: Unique name of the component + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + catalogFilter: + kind: Group + - title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com +steps: + - id: fetch-base + name: Fetch Base + action: fetch:template + input: + url: ./template + values: + name: \${{parameters.name}} +`; + +type TemplateOption = { + label: string; + value: Entity; +}; + +const useStyles = makeStyles(theme => ({ + root: { + gridArea: 'pageContent', + display: 'grid', + gridTemplateAreas: ` + "controls controls" + "textArea preview" + `, + gridTemplateRows: 'auto 1fr', + gridTemplateColumns: '1fr 1fr', + }, + controls: { + gridArea: 'controls', + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + margin: theme.spacing(1), + }, + textArea: { + gridArea: 'textArea', + }, + preview: { + gridArea: 'preview', + }, +})); + +export const TemplateFormPreviewer = ({ + defaultPreviewTemplate = EXAMPLE_TEMPLATE_PARAMS_YAML, + customFieldExtensions = [], + onClose, + layouts = [], +}: { + defaultPreviewTemplate?: string; + customFieldExtensions?: NextFieldExtensionOptions<any, any>[]; + onClose?: () => void; + layouts?: LayoutOptions[]; +}) => { + const classes = useStyles(); + const alertApi = useApi(alertApiRef); + const catalogApi = useApi(catalogApiRef); + const [selectedTemplate, setSelectedTemplate] = useState(''); + const [errorText, setErrorText] = useState<string>(); + const [templateOptions, setTemplateOptions] = useState<TemplateOption[]>([]); + const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate); + + const { loading } = useAsync( + () => + catalogApi + .getEntities({ + filter: { kind: 'template' }, + fields: [ + 'kind', + 'metadata.namespace', + 'metadata.name', + 'metadata.title', + 'spec.parameters', + 'spec.steps', + 'spec.output', + ], + }) + .then(({ items }) => + setTemplateOptions( + items.map(template => ({ + label: + template.metadata.title ?? + humanizeEntityRef(template, { defaultKind: 'template' }), + value: template, + })), + ), + ) + .catch(e => + alertApi.post({ + message: `Error loading exisiting templates: ${e.message}`, + severity: 'error', + }), + ), + [catalogApi], + ); + + const handleSelectChange = useCallback( + selected => { + setSelectedTemplate(selected); + setTemplateYaml(yaml.stringify(selected.spec)); + }, + [setTemplateYaml], + ); + + return ( + <> + {loading && <LinearProgress />} + <main className={classes.root}> + <div className={classes.controls}> + <FormControl variant="outlined" size="small" fullWidth> + <InputLabel id="select-template-label"> + Load Existing Template + </InputLabel> + <Select + value={selectedTemplate} + label="Load Existing Template" + labelId="select-template-label" + onChange={e => handleSelectChange(e.target.value)} + > + {templateOptions.map((option, idx) => ( + <MenuItem key={idx} value={option.value as any}> + {option.label} + </MenuItem> + ))} + </Select> + </FormControl> + + <IconButton size="medium" onClick={onClose}> + <CloseIcon /> + </IconButton> + </div> + <div className={classes.textArea}> + <TemplateEditorTextArea + content={templateYaml} + onUpdate={setTemplateYaml} + errorText={errorText} + /> + </div> + <div className={classes.preview}> + <TemplateEditorForm + content={templateYaml} + contentIsSpec + fieldExtensions={customFieldExtensions} + setErrorText={setErrorText} + layouts={layouts} + /> + </div> + </main> + </> + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/index.ts b/plugins/scaffolder/src/next/TemplateEditorPage/index.ts new file mode 100644 index 0000000000..7ec6bddb64 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/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 { TemplateEditorPage } from './TemplateEditorPage'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx b/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx new file mode 100644 index 0000000000..3bca46af2a --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx @@ -0,0 +1,123 @@ +/* + * 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 { useRouteRef } from '@backstage/core-plugin-api'; +import IconButton from '@material-ui/core/IconButton'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import MenuItem from '@material-ui/core/MenuItem'; +import MenuList from '@material-ui/core/MenuList'; +import Popover from '@material-ui/core/Popover'; +import { makeStyles } from '@material-ui/core/styles'; +import Description from '@material-ui/icons/Description'; +import Edit from '@material-ui/icons/Edit'; +import List from '@material-ui/icons/List'; +import MoreVert from '@material-ui/icons/MoreVert'; +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + nextActionsRouteRef, + nextEditRouteRef, + nextScaffolderListTaskRouteRef, +} from '../routes'; + +const useStyles = makeStyles({ + button: { + color: 'white', + }, +}); + +export type ScaffolderPageContextMenuProps = { + editor?: boolean; + actions?: boolean; + tasks?: boolean; +}; + +export function ContextMenu(props: ScaffolderPageContextMenuProps) { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>(); + const editLink = useRouteRef(nextEditRouteRef); + const actionsLink = useRouteRef(nextActionsRouteRef); + const tasksLink = useRouteRef(nextScaffolderListTaskRouteRef); + + const navigate = useNavigate(); + + const showEditor = props.editor !== false; + const showActions = props.actions !== false; + const showTasks = props.tasks !== false; + + if (!showEditor && !showActions) { + return null; + } + + const onOpen = (event: React.SyntheticEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget); + }; + + const onClose = () => { + setAnchorEl(undefined); + }; + + return ( + <> + <IconButton + aria-label="more" + aria-controls="long-menu" + aria-haspopup="true" + onClick={onOpen} + data-testid="menu-button" + color="inherit" + className={classes.button} + > + <MoreVert /> + </IconButton> + <Popover + open={Boolean(anchorEl)} + onClose={onClose} + anchorEl={anchorEl} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + > + <MenuList> + {showEditor && ( + <MenuItem onClick={() => navigate(editLink())}> + <ListItemIcon> + <Edit fontSize="small" /> + </ListItemIcon> + <ListItemText primary="Template Editor" /> + </MenuItem> + )} + {showActions && ( + <MenuItem onClick={() => navigate(actionsLink())}> + <ListItemIcon> + <Description fontSize="small" /> + </ListItemIcon> + <ListItemText primary="Installed Actions" /> + </MenuItem> + )} + {showTasks && ( + <MenuItem onClick={() => navigate(tasksLink())}> + <ListItemIcon> + <List fontSize="small" /> + </ListItemIcon> + <ListItemText primary="Task List" /> + </MenuItem> + )} + </MenuList> + </Popover> + </> + ); +} diff --git a/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx index 16e0a355a3..069a2cc6c5 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx @@ -21,7 +21,7 @@ import useMediaQuery from '@material-ui/core/useMediaQuery'; import React from 'react'; import { Link as RouterLink, LinkProps } from 'react-router-dom'; import AddCircleOutline from '@material-ui/icons/AddCircleOutline'; -import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; import { usePermission } from '@backstage/plugin-permission-react'; /** diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx index f76c1a4ab6..6fe2d6beb9 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx @@ -18,7 +18,7 @@ jest.mock('@backstage/plugin-catalog-react', () => ({ useEntityList: jest.fn(), })); -jest.mock('@backstage/plugin-scaffolder-react', () => ({ +jest.mock('@backstage/plugin-scaffolder-react/alpha', () => ({ TemplateGroup: jest.fn(() => null), })); @@ -27,7 +27,7 @@ import { useEntityList } from '@backstage/plugin-catalog-react'; import { TemplateGroups } from './TemplateGroups'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { errorApiRef } from '@backstage/core-plugin-api'; -import { TemplateGroup } from '@backstage/plugin-scaffolder-react'; +import { TemplateGroup } from '@backstage/plugin-scaffolder-react/alpha'; import { nextRouteRef } from '../routes'; describe('TemplateGroups', () => { diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx index 485b0e40cd..590ef802bd 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx @@ -30,7 +30,7 @@ import { useApp, useRouteRef, } from '@backstage/core-plugin-api'; -import { TemplateGroup } from '@backstage/plugin-scaffolder-react'; +import { TemplateGroup } from '@backstage/plugin-scaffolder-react/alpha'; import { viewTechDocRouteRef } from '../../routes'; import { nextSelectedTemplateRouteRef } from '../routes'; import { useNavigate } from 'react-router-dom'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index 4eae84b866..bddc15a61b 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -37,22 +37,42 @@ import { RegisterExistingButton } from './RegisterExistingButton'; import { useRouteRef } from '@backstage/core-plugin-api'; import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups'; import { registerComponentRouteRef } from '../../routes'; +import { ContextMenu } from './ContextMenu'; export type TemplateListPageProps = { TemplateCardComponent?: React.ComponentType<{ template: TemplateEntityV1beta3; }>; groups?: TemplateGroupFilter[]; + contextMenu?: { + editor?: boolean; + actions?: boolean; + tasks?: boolean; + }; }; const defaultGroup: TemplateGroupFilter = { - title: 'All Templates', + title: 'Templates', filter: () => true, }; +const createGroupsWithOther = ( + groups: TemplateGroupFilter[], +): TemplateGroupFilter[] => [ + ...groups, + { + title: 'Other Templates', + filter: e => ![...groups].some(({ filter }) => filter(e)), + }, +]; + export const TemplateListPage = (props: TemplateListPageProps) => { const registerComponentLink = useRouteRef(registerComponentRouteRef); - const { TemplateCardComponent, groups = [] } = props; + const { TemplateCardComponent, groups: givenGroups = [] } = props; + + const groups = givenGroups.length + ? createGroupsWithOther(givenGroups) + : [defaultGroup]; return ( <EntityListProvider> @@ -61,7 +81,9 @@ export const TemplateListPage = (props: TemplateListPageProps) => { pageTitleOverride="Create a new component" title="Create a new component" subtitle="Create new software components using standard templates in your organization" - /> + > + <ContextMenu {...props.contextMenu} /> + </Header> <Content> <ContentHeader title="Available Templates"> <RegisterExistingButton @@ -88,7 +110,7 @@ export const TemplateListPage = (props: TemplateListPageProps) => { </CatalogFilterLayout.Filters> <CatalogFilterLayout.Content> <TemplateGroups - groups={[...groups, defaultGroup]} + groups={groups} TemplateCardComponent={TemplateCardComponent} /> </CatalogFilterLayout.Content> diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 24c529b094..4825715fcb 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -25,28 +25,35 @@ import { import { scaffolderApiRef, useTemplateSecrets, - NextFieldExtensionOptions, + type LayoutOptions, } from '@backstage/plugin-scaffolder-react'; +import { + FormProps, + Workflow, + NextFieldExtensionOptions, +} from '@backstage/plugin-scaffolder-react/alpha'; import { JsonValue } from '@backstage/types'; -import { type FormProps } from '../types'; -import { nextRouteRef } from '../routes'; -import { scaffolderTaskRouteRef, selectedTemplateRouteRef } from '../../routes'; import { Header, Page } from '@backstage/core-components'; -import { Workflow } from '@backstage/plugin-scaffolder-react'; +import { + nextRouteRef, + nextScaffolderTaskRouteRef, + nextSelectedTemplateRouteRef, +} from '../routes'; -type TemplateWizardPageProps = { +export type TemplateWizardPageProps = { customFieldExtensions: NextFieldExtensionOptions<any, any>[]; + layouts?: LayoutOptions[]; FormProps?: FormProps; }; export const TemplateWizardPage = (props: TemplateWizardPageProps) => { const rootRef = useRouteRef(nextRouteRef); - const taskRoute = useRouteRef(scaffolderTaskRouteRef); + const taskRoute = useRouteRef(nextScaffolderTaskRouteRef); const { secrets } = useTemplateSecrets(); const scaffolderApi = useApi(scaffolderApiRef); const navigate = useNavigate(); const { templateName, namespace } = useRouteRefParams( - selectedTemplateRouteRef, + nextSelectedTemplateRouteRef, ); const templateRef = stringifyEntityRef({ @@ -82,6 +89,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { onError={onError} extensions={props.customFieldExtensions} FormProps={props.FormProps} + layouts={props.layouts} /> </Page> </AnalyticsContext> diff --git a/plugins/scaffolder/src/next/routes.ts b/plugins/scaffolder/src/next/routes.ts index 620122ee60..12fad4210a 100644 --- a/plugins/scaffolder/src/next/routes.ts +++ b/plugins/scaffolder/src/next/routes.ts @@ -34,3 +34,24 @@ export const nextScaffolderTaskRouteRef = createSubRouteRef({ parent: nextRouteRef, path: '/tasks/:taskId', }); + +/** @alpha */ +export const nextScaffolderListTaskRouteRef = createSubRouteRef({ + id: 'scaffolder/next/list-tasks', + parent: nextRouteRef, + path: '/tasks', +}); + +/** @alpha */ +export const nextActionsRouteRef = createSubRouteRef({ + id: 'scaffolder/next/actions', + parent: nextRouteRef, + path: '/actions', +}); + +/** @alpha */ +export const nextEditRouteRef = createSubRouteRef({ + id: 'scaffolder/next/edit', + parent: nextRouteRef, + path: '/edit', +}); diff --git a/plugins/scaffolder/src/next/types.ts b/plugins/scaffolder/src/next/types.ts index e9c77aa00e..c2ee473d54 100644 --- a/plugins/scaffolder/src/next/types.ts +++ b/plugins/scaffolder/src/next/types.ts @@ -23,7 +23,7 @@ import type { FormProps as SchemaFormProps } from '@rjsf/core-v5'; /** - * Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage` + * Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderPage` * * @alpha * @deprecated use the import from {@link @backstage/plugin-scaffolder-react/alpha#FormProps} instead diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index 5d4249e882..1a73eecf55 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.4-next.0 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.3 + +### Patch Changes + +- 896566906b: Export `decodeElasticSearchPageCursor` utility function +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.1 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.1.3-next.0 + ## 1.1.1 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index 9e3ca40ec7..8545e3fd8f 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -67,6 +67,11 @@ export interface BaseElasticSearchClientOptions { Transport?: ElasticSearchTransportConstructor; } +// @public (undocumented) +export function decodeElasticSearchPageCursor(pageCursor?: string): { + page: number; +}; + // @public (undocumented) export interface ElasticSearchAgentOptions { // (undocumented) diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index dc3766116a..d8e1ee7021 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "1.1.1", + "version": "1.1.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 9edad973de..c2f4224db3 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -408,6 +408,9 @@ export class ElasticSearchSearchEngine implements SearchEngine { } } +/** + * @public + */ export function decodePageCursor(pageCursor?: string): { page: number } { if (!pageCursor) { return { page: 0 }; diff --git a/plugins/search-backend-module-elasticsearch/src/engines/index.ts b/plugins/search-backend-module-elasticsearch/src/engines/index.ts index 8c3b9e3c6d..cb24bdc56a 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/index.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -export { ElasticSearchSearchEngine } from './ElasticSearchSearchEngine'; +export { + decodePageCursor as decodeElasticSearchPageCursor, + ElasticSearchSearchEngine, +} from './ElasticSearchSearchEngine'; export { isOpenSearchCompatible } from './ElasticSearchClientOptions'; export type { BaseElasticSearchClientOptions, diff --git a/plugins/search-backend-module-elasticsearch/src/index.ts b/plugins/search-backend-module-elasticsearch/src/index.ts index cb4f62098a..e0b05014cc 100644 --- a/plugins/search-backend-module-elasticsearch/src/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/index.ts @@ -20,7 +20,11 @@ * @packageDocumentation */ -export { ElasticSearchSearchEngine, isOpenSearchCompatible } from './engines'; +export { + decodeElasticSearchPageCursor, + ElasticSearchSearchEngine, + isOpenSearchCompatible, +} from './engines'; export type { BaseElasticSearchClientOptions, ElasticSearchAgentOptions, diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 7f6888ac53..abfc6e5060 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.4-next.0 + - @backstage/plugin-search-common@1.2.1 + +## 0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3 + - @backstage/plugin-search-common@1.2.1 + +## 0.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + +## 0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.1 + - @backstage/plugin-search-common@1.2.1 + +## 0.5.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-search-backend-node@1.1.3-next.0 + ## 0.5.1 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index acc0d6fa01..f944cf7c59 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.5.1", + "version": "0.5.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-pg/src/database/util.test.ts b/plugins/search-backend-module-pg/src/database/util.test.ts index 09eb70c096..b3df5a3ad4 100644 --- a/plugins/search-backend-module-pg/src/database/util.test.ts +++ b/plugins/search-backend-module-pg/src/database/util.test.ts @@ -50,7 +50,7 @@ describe('util', () => { 'should get postgres major version, %p', async databaseId => { const knex = await databases.init(databaseId); - const expectedVersion = +databaseId.substr(9); + const expectedVersion = +databaseId.slice(9); await expect(queryPostgresMajorVersion(knex)).resolves.toBe( expectedVersion, diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index a56f7aa349..eedafe3497 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-search-backend-node +## 1.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + ## 1.1.1 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index e82319fdb6..f8ec1c00b0 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "1.1.1", + "version": "1.1.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index fecb4cb9ea..3bd3b84554 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/plugin-search-backend +## 1.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.6-next.0 + - @backstage/plugin-search-backend-node@1.1.4-next.0 + - @backstage/plugin-search-common@1.2.1 + +## 1.2.3 + +### Patch Changes + +- 1b30c9a0f7: Change the router's response to include the error message instead of its object type in case it fails during a search query because the messages have more info. +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5 + - @backstage/plugin-search-backend-node@1.1.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.2.3-next.2 + +### Patch Changes + +- 1b30c9a0f7: Change the router's response to include the error message instead of its object type in case it fails during a search query because the messages have more info. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + +## 1.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-node@0.7.5-next.1 + - @backstage/plugin-search-backend-node@1.1.3-next.1 + - @backstage/plugin-search-common@1.2.1 + +## 1.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + - @backstage/plugin-permission-node@0.7.5-next.0 + - @backstage/plugin-search-backend-node@1.1.3-next.0 + ## 1.2.1 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 54b374011f..4eb8187a2a 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "1.2.1", + "version": "1.2.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 1dbd145bce..b4772dd62d 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -74,6 +74,23 @@ describe('createRouter', () => { }); describe('GET /query', () => { + it('throws meaningful query errors', async () => { + const error = new Error('Query error message'); + mockSearchEngine.query.mockRejectedValueOnce(error); + + const response = await request(app).get('/query'); + + expect(response.status).toEqual(500); + expect(response.body).toMatchObject( + expect.objectContaining({ + error: { + name: 'Error', + message: `There was a problem performing the search query: ${error.message}`, + }, + }), + ); + }); + it('returns empty results array', async () => { const response = await request(app).get('/query'); diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index b51dcead14..10f47d180f 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -184,7 +184,7 @@ export async function createRouter( } throw new Error( - `There was a problem performing the search query. ${error}`, + `There was a problem performing the search query: ${error.message}`, ); } }, diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 37214bdb82..52ac478207 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,71 @@ # @backstage/plugin-search-react +## 1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.5.0 + +### Minor Changes + +- 0eaa579f89: - Create the search results extensions, for more details see the documentation [here](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions); + - Update the `SearchResult`, `SearchResultList` and `SearchResultGroup` components to use extensions and default their props to optionally accept a query, when the query is not passed, the component tries to get it from the search context. + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.5.0-next.1 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.5.0-next.0 + +### Minor Changes + +- 0eaa579f89: - Create the search results extensions, for more details see the documentation [here](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions); + - Update the `SearchResult`, `SearchResultList` and `SearchResultGroup` components to use extensions and default their props to optionally accept a query, when the query is not passed, the component tries to get it from the search context. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + ## 1.4.0 ### Minor Changes diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 3d1e4091ea..375af20d5c 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -8,11 +8,13 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; import { AutocompleteProps } from '@material-ui/lab'; +import { Extension } from '@backstage/core-plugin-api'; import { ForwardRefExoticComponent } from 'react'; import { InputBaseProps } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { LinkProps } from '@backstage/core-components'; +import { ListItemProps } from '@material-ui/core'; import { ListItemTextProps } from '@material-ui/core'; import { ListProps } from '@material-ui/core'; import { PropsWithChildren } from 'react'; @@ -34,6 +36,13 @@ export const AutocompleteFilter: ( // @public (undocumented) export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; +// @public +export const createSearchResultListItemExtension: < + Component extends (props: any) => JSX.Element | null, +>( + options: SearchResultListItemExtensionOptions<Component>, +) => Extension<Component>; + // @public (undocumented) export const DefaultResultListItem: ( props: DefaultResultListItemProps, @@ -43,10 +52,11 @@ export const DefaultResultListItem: ( export type DefaultResultListItemProps = { icon?: ReactNode; secondaryAction?: ReactNode; - result: SearchDocument; + result?: SearchDocument; highlight?: ResultHighlight; rank?: number; lineClamp?: number; + toggleModal?: () => void; }; // @public (undocumented) @@ -280,7 +290,10 @@ export const SearchResultContext: ( // @public export type SearchResultContextProps = { - children: (state: AsyncState<SearchResultSet>) => JSX.Element | null; + children: ( + state: AsyncState<SearchResultSet>, + query: Partial<SearchQuery>, + ) => JSX.Element | null; }; // @public @@ -313,6 +326,8 @@ export function SearchResultGroupLayout<FilterOption>( // @public export type SearchResultGroupLayoutProps<FilterOption> = ListProps & { + error?: Error; + loading?: boolean; icon: JSX.Element; title: ReactNode; titleProps?: Partial<TypographyProps>; @@ -332,19 +347,19 @@ export type SearchResultGroupLayoutProps<FilterOption> = ListProps & { index: number, array: SearchResult_2[], ) => JSX.Element | null; - error?: Error; - loading?: boolean; noResultsComponent?: ReactNode; + disableRenderingWithNoResults?: boolean; }; // @public -export type SearchResultGroupProps<FilterOption> = Omit< - SearchResultGroupLayoutProps<FilterOption>, - 'loading' | 'error' | 'resultItems' | 'filterFields' -> & { - query: Partial<SearchQuery>; - disableRenderingWithNoResults?: boolean; -}; +export type SearchResultGroupProps<FilterOption> = Pick< + SearchResultStateProps, + 'query' +> & + Omit< + SearchResultGroupLayoutProps<FilterOption>, + 'loading' | 'error' | 'resultItems' | 'filterFields' + >; // @public export const SearchResultGroupSelectFilterField: ( @@ -369,6 +384,35 @@ export type SearchResultGroupTextFilterFieldProps = // @public export const SearchResultList: (props: SearchResultListProps) => JSX.Element; +// @public +export type SearchResultListItemExtensionOptions< + Component extends (props: any) => JSX.Element | null, +> = { + name: string; + component: () => Promise<Component>; + predicate?: (result: SearchResult_2) => boolean; +}; + +// @public +export type SearchResultListItemExtensionProps<Props extends {} = {}> = Props & + PropsWithChildren< + { + rank?: number; + result?: SearchDocument; + noTrack?: boolean; + } & Omit<ListItemProps, 'button'> + >; + +// @public +export const SearchResultListItemExtensions: ( + props: SearchResultListItemExtensionsProps, +) => JSX.Element; + +// @public +export type SearchResultListItemExtensionsProps = Omit<ListProps, 'results'> & { + results: SearchResult_2[]; +}; + // @public export const SearchResultListLayout: ( props: SearchResultListLayoutProps, @@ -376,34 +420,31 @@ export const SearchResultListLayout: ( // @public export type SearchResultListLayoutProps = ListProps & { + error?: Error; + loading?: boolean; resultItems?: SearchResult_2[]; renderResultItem?: ( value: SearchResult_2, index: number, array: SearchResult_2[], ) => JSX.Element | null; - error?: Error; - loading?: boolean; noResultsComponent?: ReactNode; + disableRenderingWithNoResults?: boolean; }; // @public -export type SearchResultListProps = Omit< - SearchResultListLayoutProps, - 'loading' | 'error' | 'resultItems' -> & { - query: Partial<SearchQuery>; - disableRenderingWithNoResults?: boolean; -}; +export type SearchResultListProps = Pick<SearchResultStateProps, 'query'> & + Omit<SearchResultListLayoutProps, 'loading' | 'error' | 'resultItems'>; // @public (undocumented) export const SearchResultPager: () => JSX.Element; // @public -export type SearchResultProps = Pick<SearchResultStateProps, 'query'> & { - children: (resultSet: SearchResultSet) => JSX.Element; - noResultsComponent?: JSX.Element; -}; +export type SearchResultProps = Pick<SearchResultStateProps, 'query'> & + Omit<SearchResultListItemExtensionsProps, 'results' | 'children'> & { + children?: ReactNode | ((resultSet: SearchResultSet) => JSX.Element); + noResultsComponent?: JSX.Element; + }; // @public export const SearchResultState: (props: SearchResultStateProps) => JSX.Element; @@ -420,4 +461,9 @@ export const useSearch: () => SearchContextValue; // @public export const useSearchContextCheck: () => boolean; + +// @public +export const useSearchResultListItemExtensions: ( + children: ReactNode, +) => (result: SearchResult_2, key?: number) => JSX.Element; ``` diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index e69ec2e7ae..fd8242b552 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.4.0", + "version": "1.5.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx index a790c12a8a..7049b7f4ef 100644 --- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Button } from '@backstage/core-components'; +import { LinkButton } from '@backstage/core-components'; import { lightTheme } from '@backstage/theme'; import { Grid } from '@material-ui/core'; import CssBaseline from '@material-ui/core/CssBaseline'; @@ -66,7 +66,7 @@ export const WithSecondaryAction = () => { <DefaultResultListItem result={mockSearchResult} secondaryAction={ - <Button + <LinkButton to="#" size="small" aria-label="owner" @@ -75,7 +75,7 @@ export const WithSecondaryAction = () => { style={{ textTransform: 'lowercase' }} > {mockSearchResult.owner} - </Button> + </LinkButton> } /> ); diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx index e94d394639..aab105df48 100644 --- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx @@ -35,9 +35,10 @@ describe('DefaultResultListItem', () => { it('Includes primary/secondary text (title / text)', async () => { await renderInTestApp(<DefaultResultListItem result={result} />); - expect(screen.getByRole('listitem')).toHaveTextContent( - result.title + result.text, - ); + expect( + screen.getByRole('link', { name: result.title }), + ).toBeInTheDocument(); + expect(screen.getByText(result.text)).toBeInTheDocument(); }); it('should render icon if prop is specified', async () => { diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx index 6158aed008..cf89b6a9cd 100644 --- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -15,19 +15,13 @@ */ import React, { ReactNode } from 'react'; -import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api'; +import { AnalyticsContext } from '@backstage/core-plugin-api'; import { ResultHighlight, SearchDocument, } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '../HighlightedSearchResultText'; -import { - ListItem, - ListItemIcon, - ListItemText, - Box, - Divider, -} from '@material-ui/core'; +import { ListItemIcon, ListItemText, Box } from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; import { Link } from '@backstage/core-components'; @@ -39,10 +33,11 @@ import { Link } from '@backstage/core-components'; export type DefaultResultListItemProps = { icon?: ReactNode; secondaryAction?: ReactNode; - result: SearchDocument; + result?: SearchDocument; highlight?: ResultHighlight; rank?: number; lineClamp?: number; + toggleModal?: () => void; }; /** @@ -53,63 +48,53 @@ export type DefaultResultListItemProps = { export const DefaultResultListItemComponent = ({ result, highlight, - rank, icon, secondaryAction, lineClamp = 5, }: DefaultResultListItemProps) => { - const analytics = useAnalytics(); - const handleClick = () => { - analytics.captureEvent('discover', result.title, { - attributes: { to: result.location }, - value: rank, - }); - }; + if (!result) return null; return ( <> - <ListItem alignItems="center"> - {icon && <ListItemIcon>{icon}</ListItemIcon>} - <ListItemText - primaryTypographyProps={{ variant: 'h6' }} - primary={ - <Link noTrack to={result.location} onClick={handleClick}> - {highlight?.fields.title ? ( - <HighlightedSearchResultText - text={highlight?.fields.title || ''} - preTag={highlight?.preTag || ''} - postTag={highlight?.postTag || ''} - /> - ) : ( - result.title - )} - </Link> - } - secondary={ - <Typography - component="span" - style={{ - display: '-webkit-box', - WebkitBoxOrient: 'vertical', - WebkitLineClamp: lineClamp, - overflow: 'hidden', - }} - > - {highlight?.fields.text ? ( - <HighlightedSearchResultText - text={highlight.fields.text} - preTag={highlight.preTag} - postTag={highlight.postTag} - /> - ) : ( - result.text - )} - </Typography> - } - /> - {secondaryAction && <Box alignItems="flex-end">{secondaryAction}</Box>} - </ListItem> - <Divider /> + {icon && <ListItemIcon>{icon}</ListItemIcon>} + <ListItemText + primaryTypographyProps={{ variant: 'h6' }} + primary={ + <Link noTrack to={result.location}> + {highlight?.fields.title ? ( + <HighlightedSearchResultText + text={highlight?.fields.title || ''} + preTag={highlight?.preTag || ''} + postTag={highlight?.postTag || ''} + /> + ) : ( + result.title + )} + </Link> + } + secondary={ + <Typography + component="span" + style={{ + display: '-webkit-box', + WebkitBoxOrient: 'vertical', + WebkitLineClamp: lineClamp, + overflow: 'hidden', + }} + > + {highlight?.fields.text ? ( + <HighlightedSearchResultText + text={highlight.fields.text} + preTag={highlight.preTag} + postTag={highlight.postTag} + /> + ) : ( + result.text + )} + </Typography> + } + /> + {secondaryAction && <Box alignItems="flex-end">{secondaryAction}</Box>} </> ); }; diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx index ef9b0d6b5c..f3086fc40a 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx @@ -15,24 +15,25 @@ */ import React, { ComponentType } from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { List, ListItem } from '@material-ui/core'; import DefaultIcon from '@material-ui/icons/InsertDriveFile'; import CustomIcon from '@material-ui/icons/NoteAdd'; import { Link } from '@backstage/core-components'; -import { TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; +import { createPlugin } from '@backstage/core-plugin-api'; import { SearchDocument } from '@backstage/plugin-search-common'; -import { searchApiRef, MockSearchApi } from '../../api'; import { SearchContextProvider } from '../../context'; +import { searchApiRef, MockSearchApi } from '../../api'; +import { createSearchResultListItemExtension } from '../../extensions'; -import { DefaultResultListItem } from '../DefaultResultListItem'; import { SearchResultListLayout } from '../SearchResultList'; +import { SearchResultGroupLayout } from '../SearchResultGroup'; +import { DefaultResultListItem } from '../DefaultResultListItem'; import { SearchResult } from './SearchResult'; -import { SearchResultGroupLayout } from '../SearchResultGroup'; const mockResults = { results: [ @@ -69,15 +70,14 @@ export default { title: 'Plugins/Search/SearchResult', component: SearchResult, decorators: [ - (Story: ComponentType<{}>) => ( - <MemoryRouter> + (Story: ComponentType<{}>) => + wrapInTestApp( <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> <SearchContextProvider> <Story /> </SearchContextProvider> - </TestApiProvider> - </MemoryRouter> - ), + </TestApiProvider>, + ), ], }; @@ -247,3 +247,18 @@ export const WithCustomNoResultsComponent = () => { </SearchResult> ); }; + +export const UsingSearchResultItemExtensions = () => { + const plugin = createPlugin({ id: 'plugin' }); + const DefaultResultItem = plugin.provide( + createSearchResultListItemExtension({ + name: 'DefaultResultListItem', + component: async () => DefaultResultListItem, + }), + ); + return ( + <SearchResult> + <DefaultResultItem /> + </SearchResult> + ); +}; diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx index 5972eaa891..4b8caa2780 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx @@ -16,10 +16,18 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; +import { + wrapInTestApp, + renderInTestApp, + renderWithEffects, + TestApiProvider, +} from '@backstage/test-utils'; +import { createPlugin } from '@backstage/core-plugin-api'; -import { renderInTestApp } from '@backstage/test-utils'; - +import { searchApiRef } from '../../api'; import { useSearch } from '../../context'; +import { createSearchResultListItemExtension } from '../../extensions'; + import { SearchResult } from './SearchResult'; jest.mock('../../context', () => ({ @@ -30,8 +38,12 @@ jest.mock('../../context', () => ({ })); describe('SearchResult', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('Progress rendered on Loading state', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ + (useSearch as jest.Mock).mockReturnValue({ result: { loading: true }, }); @@ -46,7 +58,7 @@ describe('SearchResult', () => { it('Alert rendered on Error state', async () => { const error = new Error('some error'); - (useSearch as jest.Mock).mockReturnValueOnce({ + (useSearch as jest.Mock).mockReturnValue({ result: { loading: false, error }, }); @@ -62,7 +74,7 @@ describe('SearchResult', () => { }); it('On no result value state', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ + (useSearch as jest.Mock).mockReturnValue({ result: { loading: false, error: '', value: undefined }, }); @@ -78,7 +90,7 @@ describe('SearchResult', () => { }); it('On empty result value state', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ + (useSearch as jest.Mock).mockReturnValue({ result: { loading: false, error: '', value: { results: [] } }, }); @@ -94,7 +106,7 @@ describe('SearchResult', () => { }); it('On empty result value state with custom component', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ + (useSearch as jest.Mock).mockReturnValue({ result: { loading: false, error: '', value: { results: [] } }, }); @@ -110,7 +122,7 @@ describe('SearchResult', () => { }); it('Calls children with results set to result.value', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ + (useSearch as jest.Mock).mockReturnValue({ result: { loading: false, error: '', @@ -140,4 +152,81 @@ describe('SearchResult', () => { expect(getByText('Results 1')).toBeInTheDocument(); }); + + it('Renders results from api', async () => { + const results = [ + { + type: 'some-type', + document: { + title: 'some-title', + text: 'some-text', + location: 'some-location', + }, + }, + ]; + const query = jest.fn().mockResolvedValue({ results }); + await renderWithEffects( + wrapInTestApp( + <TestApiProvider apis={[[searchApiRef, { query }]]}> + <SearchResult query={{ types: ['techdocs'] }}> + {value => { + expect(value.results).toStrictEqual(results); + return <></>; + }} + </SearchResult> + </TestApiProvider>, + ), + ); + + expect(query).toHaveBeenCalledWith({ + term: '', + filters: {}, + types: ['techdocs'], + }); + }); + + it('Renders using search result item extensions', async () => { + (useSearch as jest.Mock).mockReturnValue({ + result: { + loading: false, + error: '', + value: { + totalCount: 1, + results: [ + { + type: 'some-type', + document: { + title: 'some-title', + text: 'some-text', + location: 'some-location', + }, + }, + ], + }, + }, + }); + + const { getByText, rerender } = await renderInTestApp(<SearchResult />); + + expect(getByText('some-title')).toBeInTheDocument(); + + const SearchResultExtension = createPlugin({ + id: 'plugin', + }).provide( + createSearchResultListItemExtension({ + name: 'SearchResultExtension', + component: async () => props => <>Result: {props.result?.title}</>, + }), + ); + + rerender( + <SearchResult> + <SearchResultExtension /> + </SearchResult>, + ); + + await waitFor(() => { + expect(getByText('Result: some-title')).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx index 333156aab9..f362ff27d3 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -14,19 +14,24 @@ * limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import useAsync, { AsyncState } from 'react-use/lib/useAsync'; +import { isFunction } from 'lodash'; import { - EmptyState, Progress, + EmptyState, ResponseErrorPanel, } from '@backstage/core-components'; -import { AnalyticsContext, useApi } from '@backstage/core-plugin-api'; +import { useApi, AnalyticsContext } from '@backstage/core-plugin-api'; import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; -import { useSearch } from '../../context'; import { searchApiRef } from '../../api'; +import { useSearch } from '../../context'; +import { + SearchResultListItemExtensions, + SearchResultListItemExtensionsProps, +} from '../../extensions'; /** * Props for {@link SearchResultContext} @@ -36,7 +41,10 @@ export type SearchResultContextProps = { /** * A child function that receives an asynchronous result set and returns a react element. */ - children: (state: AsyncState<SearchResultSet>) => JSX.Element | null; + children: ( + state: AsyncState<SearchResultSet>, + query: Partial<SearchQuery>, + ) => JSX.Element | null; }; /** @@ -62,8 +70,8 @@ export type SearchResultContextProps = { export const SearchResultContext = (props: SearchResultContextProps) => { const { children } = props; const context = useSearch(); - const state = context.result; - return children(state); + const { result: state, ...query } = context; + return children(state, query); }; /** @@ -103,7 +111,7 @@ export const SearchResultApi = (props: SearchResultApiProps) => { return searchApi.query({ ...rest, term, types, filters }); }, [query]); - return children(state); + return children(state, query); }; /** @@ -165,10 +173,11 @@ export const SearchResultState = (props: SearchResultStateProps) => { * Props for {@link SearchResult} * @public */ -export type SearchResultProps = Pick<SearchResultStateProps, 'query'> & { - children: (resultSet: SearchResultSet) => JSX.Element; - noResultsComponent?: JSX.Element; -}; +export type SearchResultProps = Pick<SearchResultStateProps, 'query'> & + Omit<SearchResultListItemExtensionsProps, 'results' | 'children'> & { + children?: ReactNode | ((resultSet: SearchResultSet) => JSX.Element); + noResultsComponent?: JSX.Element; + }; /** * Renders results from a parent search context or api. @@ -183,6 +192,7 @@ export const SearchResultComponent = (props: SearchResultProps) => { noResultsComponent = ( <EmptyState missing="data" title="Sorry, no results were found" /> ), + ...rest } = props; return ( @@ -205,7 +215,15 @@ export const SearchResultComponent = (props: SearchResultProps) => { return noResultsComponent; } - return children(value); + if (isFunction(children)) { + return children(value); + } + + return ( + <SearchResultListItemExtensions {...rest} results={value.results}> + {children} + </SearchResultListItemExtensions> + ); }} </SearchResultState> ); diff --git a/plugins/search-react/src/components/SearchResult/index.tsx b/plugins/search-react/src/components/SearchResult/index.tsx index eab599883b..cf36b7ec9e 100644 --- a/plugins/search-react/src/components/SearchResult/index.tsx +++ b/plugins/search-react/src/components/SearchResult/index.tsx @@ -18,8 +18,8 @@ export { SearchResult, SearchResultApi, SearchResultContext, - SearchResultState, SearchResultComponent, + SearchResultState, } from './SearchResult'; export type { diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx index fab807dd84..40f7d30ff4 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx @@ -27,11 +27,15 @@ import DocsIcon from '@material-ui/icons/InsertDriveFile'; import { JsonValue } from '@backstage/types'; import { Link } from '@backstage/core-components'; -import { createRouteRef } from '@backstage/core-plugin-api'; -import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; +import { createPlugin, createRouteRef } from '@backstage/core-plugin-api'; +import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; +import { DefaultResultListItem } from '../DefaultResultListItem'; + +import { SearchContextProvider } from '../../context'; import { searchApiRef, MockSearchApi } from '../../api'; +import { createSearchResultListItemExtension } from '../../extensions'; import { SearchResultGroup, @@ -83,6 +87,14 @@ export default { }; export const Default = () => { + return ( + <SearchContextProvider> + <SearchResultGroup icon={<DocsIcon />} title="Documentation" /> + </SearchContextProvider> + ); +}; + +export const WithQuery = () => { const [query] = useState<Partial<SearchQuery>>({ types: ['techdocs'], }); @@ -341,3 +353,21 @@ export const WithCustomResultItem = () => { /> ); }; + +export const WithResultItemExtensions = () => { + const [query] = useState<Partial<SearchQuery>>({ + types: ['techdocs'], + }); + const plugin = createPlugin({ id: 'plugin' }); + const DefaultSearchResultGroupItem = plugin.provide( + createSearchResultListItemExtension({ + name: 'DefaultResultListItem', + component: async () => DefaultResultListItem, + }), + ); + return ( + <SearchResultGroup query={query} icon={<DocsIcon />} title="Documentation"> + <DefaultSearchResultGroupItem /> + </SearchResultGroup> + ); +}; diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx index 7943dba3f7..8a9e8986b8 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx @@ -22,12 +22,17 @@ import { MenuItem } from '@material-ui/core'; import DocsIcon from '@material-ui/icons/InsertDriveFile'; import { - TestApiProvider, - renderWithEffects, wrapInTestApp, + renderWithEffects, + TestApiProvider, + MockAnalyticsApi, } from '@backstage/test-utils'; +import { createPlugin, analyticsApiRef } from '@backstage/core-plugin-api'; import { searchApiRef } from '../../api'; +import { SearchContextProvider } from '../../context'; +import { createSearchResultListItemExtension } from '../../extensions'; + import { SearchResultGroup, SearchResultGroupSelectFilterField, @@ -36,6 +41,7 @@ import { const query = jest.fn().mockResolvedValue({ results: [] }); const searchApiMock = { query }; +const analyticsApiMock = new MockAnalyticsApi(); describe('SearchResultGroup', () => { const results = [ @@ -62,9 +68,18 @@ describe('SearchResultGroup', () => { }); it('Renders without exploding', async () => { + query.mockResolvedValueOnce({ + results, + }); + await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'] }} icon={<DocsIcon titleAccess="Docs icon" />} @@ -84,10 +99,93 @@ describe('SearchResultGroup', () => { }); }); - it('Defines a default link', async () => { + it('Renders search results from context', async () => { + query.mockResolvedValueOnce({ + results, + }); + await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > + <SearchContextProvider> + <SearchResultGroup + icon={<DocsIcon titleAccess="Docs icon" />} + title="Documentation" + /> + </SearchContextProvider> + </TestApiProvider>, + ), + ); + + expect(screen.getByText('Search Result 1')).toBeInTheDocument(); + expect( + screen.getByText('Some text from the search result 1'), + ).toBeInTheDocument(); + + expect(screen.getByText('Search Result 2')).toBeInTheDocument(); + expect( + screen.getByText('Some text from the search result 2'), + ).toBeInTheDocument(); + }); + + it('Renders search results using extensions', async () => { + query.mockResolvedValueOnce({ + results, + }); + + const SearchResultGroupItemExtension = createPlugin({ + id: 'plugin', + }).provide( + createSearchResultListItemExtension({ + name: 'SearchResultGroupItemExtension', + component: async () => props => <>Result: {props.result?.title}</>, + }), + ); + + await renderWithEffects( + wrapInTestApp( + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > + <SearchResultGroup + query={{ types: ['techdocs'] }} + icon={<DocsIcon titleAccess="Docs icon" />} + title="Documentation" + > + <SearchResultGroupItemExtension /> + </SearchResultGroup> + </TestApiProvider>, + ), + ); + + await waitFor(() => { + expect(screen.getByText('Result: Search Result 1')).toBeInTheDocument(); + }); + + expect(screen.getByText('Result: Search Result 2')).toBeInTheDocument(); + }); + + it('Defines a default link', async () => { + query.mockResolvedValueOnce({ + results, + }); + + await renderWithEffects( + wrapInTestApp( + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'] }} icon={<DocsIcon titleAccess="Docs icon" />} @@ -108,7 +206,12 @@ describe('SearchResultGroup', () => { await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'] }} icon={<DocsIcon titleAccess="Docs icon" />} @@ -132,7 +235,12 @@ describe('SearchResultGroup', () => { it('Could be customized with no results text', async () => { await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'] }} icon={<DocsIcon titleAccess="Docs icon" />} @@ -154,7 +262,12 @@ describe('SearchResultGroup', () => { await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'] }} icon={<DocsIcon titleAccess="Docs icon" />} @@ -184,7 +297,12 @@ describe('SearchResultGroup', () => { await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'], @@ -232,7 +350,12 @@ describe('SearchResultGroup', () => { await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'], @@ -276,7 +399,12 @@ describe('SearchResultGroup', () => { query.mockReturnValueOnce(new Promise(() => {})); await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'] }} icon={<DocsIcon titleAccess="Docs icon" />} @@ -295,7 +423,12 @@ describe('SearchResultGroup', () => { query.mockResolvedValueOnce({ results: [] }); await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'] }} icon={<DocsIcon titleAccess="Docs icon" />} @@ -315,7 +448,12 @@ describe('SearchResultGroup', () => { query.mockResolvedValueOnce({ results: [] }); await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'] }} icon={<DocsIcon titleAccess="Docs icon" />} @@ -335,7 +473,12 @@ describe('SearchResultGroup', () => { query.mockRejectedValueOnce(new Error()); await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultGroup query={{ types: ['techdocs'] }} icon={<DocsIcon titleAccess="Docs icon" />} diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx index 6040dbe560..2a3ae1e24c 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx @@ -27,9 +27,8 @@ import { makeStyles, Theme, List, - ListSubheader, - ListItem, ListProps, + ListSubheader, Menu, MenuItem, InputBase, @@ -43,17 +42,19 @@ import ArrowRightIcon from '@material-ui/icons/ArrowForwardIos'; import { JsonValue } from '@backstage/types'; import { - EmptyState, Link, LinkProps, Progress, + EmptyState, ResponseErrorPanel, } from '@backstage/core-components'; import { AnalyticsContext } from '@backstage/core-plugin-api'; -import { SearchQuery, SearchResult } from '@backstage/plugin-search-common'; +import { SearchResult } from '@backstage/plugin-search-common'; + +import { useSearchResultListItemExtensions } from '../../extensions'; import { DefaultResultListItem } from '../DefaultResultListItem'; -import { SearchResultState } from '../SearchResult'; +import { SearchResultState, SearchResultStateProps } from '../SearchResult'; const useStyles = makeStyles((theme: Theme) => ({ listSubheader: { @@ -278,6 +279,14 @@ export const SearchResultGroupSelectFilterField = ( * @public */ export type SearchResultGroupLayoutProps<FilterOption> = ListProps & { + /** + * If defined, will render a default error panel. + */ + error?: Error; + /** + * If defined, will render a default loading progress. + */ + loading?: boolean; /** * Icon that representing a result group. */ @@ -331,18 +340,14 @@ export type SearchResultGroupLayoutProps<FilterOption> = ListProps & { index: number, array: SearchResult[], ) => JSX.Element | null; - /** - * If defined, will render a default error panel. - */ - error?: Error; - /** - * If defined, will render a default loading progress. - */ - loading?: boolean; /** * Optional component to render when no results. Default to <EmptyState /> component. */ noResultsComponent?: ReactNode; + /** + * Optional property to provide if component should not render the component when no results are found. + */ + disableRenderingWithNoResults?: boolean; }; /** @@ -357,8 +362,8 @@ export function SearchResultGroupLayout<FilterOption>( const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const { - loading, error, + loading, icon, title, titleProps = {}, @@ -384,7 +389,8 @@ export function SearchResultGroupLayout<FilterOption>( result={resultItem.document} /> ), - noResultsComponent = ( + disableRenderingWithNoResults, + noResultsComponent = disableRenderingWithNoResults ? null : ( <EmptyState missing="data" title="Sorry, no results were found" /> ), ...rest @@ -398,6 +404,23 @@ export function SearchResultGroupLayout<FilterOption>( setAnchorEl(null); }, []); + if (loading) { + return <Progress />; + } + + if (error) { + return ( + <ResponseErrorPanel + title="Error encountered while fetching search results" + error={error} + /> + ); + } + + if (!resultItems?.length) { + return <>{noResultsComponent}</>; + } + return ( <List {...rest}> <ListSubheader className={classes.listSubheader}> @@ -440,19 +463,7 @@ export function SearchResultGroupLayout<FilterOption>( {link} </Link> </ListSubheader> - {loading ? <Progress /> : null} - {!loading && error ? ( - <ResponseErrorPanel - title="Error encountered while fetching search results" - error={error} - /> - ) : null} - {!loading && !error && resultItems?.length - ? resultItems.map(renderResultItem) - : null} - {!loading && !error && !resultItems?.length ? ( - <ListItem>{noResultsComponent}</ListItem> - ) : null} + {resultItems.map(renderResultItem)} </List> ); } @@ -461,19 +472,14 @@ export function SearchResultGroupLayout<FilterOption>( * Props for {@link SearchResultGroup}. * @public */ -export type SearchResultGroupProps<FilterOption> = Omit< - SearchResultGroupLayoutProps<FilterOption>, - 'loading' | 'error' | 'resultItems' | 'filterFields' -> & { - /** - * A search query used for requesting the results to be grouped. - */ - query: Partial<SearchQuery>; - /** - * Optional property to provide if component should not render the group when no results are found. - */ - disableRenderingWithNoResults?: boolean; -}; +export type SearchResultGroupProps<FilterOption> = Pick< + SearchResultStateProps, + 'query' +> & + Omit< + SearchResultGroupLayoutProps<FilterOption>, + 'loading' | 'error' | 'resultItems' | 'filterFields' + >; /** * Given a query, search for results and render them as a group. @@ -483,22 +489,9 @@ export type SearchResultGroupProps<FilterOption> = Omit< export function SearchResultGroup<FilterOption>( props: SearchResultGroupProps<FilterOption>, ) { - const { - query, - linkProps = {}, - disableRenderingWithNoResults, - ...rest - } = props; + const { query, children, renderResultItem, linkProps = {}, ...rest } = props; - const to = `/search?${qs.stringify( - { - query: query.term, - types: query.types, - filters: query.filters, - pageCursor: query.pageCursor, - }, - { arrayFormat: 'brackets' }, - )}`; + const defaultRenderResultItem = useSearchResultListItemExtensions(children); return ( <AnalyticsContext @@ -508,19 +501,24 @@ export function SearchResultGroup<FilterOption>( }} > <SearchResultState query={query}> - {({ loading, error, value }) => { - if (!value?.results?.length && disableRenderingWithNoResults) { - return null; - } + {( + { loading, error, value }, + { term, types, pageCursor, filters = {} }, + ) => { + const to = `/search?${qs.stringify( + { term, types, filters, pageCursor, query: term }, + { arrayFormat: 'brackets' }, + )}`; return ( <SearchResultGroupLayout {...rest} - loading={loading} error={error} + loading={loading} linkProps={{ to, ...linkProps }} + filterFields={Object.keys(filters)} resultItems={value?.results} - filterFields={Object.keys(query.filters ?? {})} + renderResultItem={renderResultItem ?? defaultRenderResultItem} /> ); }} diff --git a/plugins/search-react/src/components/SearchResultList/SearchResultList.stories.tsx b/plugins/search-react/src/components/SearchResultList/SearchResultList.stories.tsx index fcda4cec87..4369b6636e 100644 --- a/plugins/search-react/src/components/SearchResultList/SearchResultList.stories.tsx +++ b/plugins/search-react/src/components/SearchResultList/SearchResultList.stories.tsx @@ -18,12 +18,14 @@ import React, { ComponentType, useState } from 'react'; import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; -import { createRouteRef } from '@backstage/core-plugin-api'; import { CatalogIcon, Link } from '@backstage/core-components'; -import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; +import { createPlugin, createRouteRef } from '@backstage/core-plugin-api'; +import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; +import { SearchContextProvider } from '../../context'; import { searchApiRef, MockSearchApi } from '../../api'; +import { createSearchResultListItemExtension } from '../../extensions'; import { SearchResultList } from './SearchResultList'; import { DefaultResultListItem } from '../DefaultResultListItem'; @@ -72,6 +74,14 @@ export default { }; export const Default = () => { + return ( + <SearchContextProvider> + <SearchResultList /> + </SearchContextProvider> + ); +}; + +export const WithQuery = () => { const [query] = useState<Partial<SearchQuery>>({ types: ['techdocs'], }); @@ -195,3 +205,21 @@ export const WithCustomResultItem = () => { /> ); }; + +export const WithResultItemExtensions = () => { + const [query] = useState<Partial<SearchQuery>>({ + types: ['techdocs'], + }); + const plugin = createPlugin({ id: 'plugin' }); + const DefaultSearchResultListItem = plugin.provide( + createSearchResultListItemExtension({ + name: 'DefaultResultListItem', + component: async () => DefaultResultListItem, + }), + ); + return ( + <SearchResultList query={query}> + <DefaultSearchResultListItem /> + </SearchResultList> + ); +}; diff --git a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx index 93bbaa5602..a0f5bc7830 100644 --- a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx +++ b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx @@ -21,13 +21,18 @@ import { TestApiProvider, renderWithEffects, wrapInTestApp, + MockAnalyticsApi, } from '@backstage/test-utils'; +import { analyticsApiRef, createPlugin } from '@backstage/core-plugin-api'; import { searchApiRef } from '../../api'; +import { createSearchResultListItemExtension } from '../../extensions'; + import { SearchResultList } from './SearchResultList'; const query = jest.fn().mockResolvedValue({ results: [] }); const searchApiMock = { query }; +const analyticsApiMock = new MockAnalyticsApi(); describe('SearchResultList', () => { const results = [ @@ -56,7 +61,12 @@ describe('SearchResultList', () => { it('Renders without exploding', async () => { await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultList query={{ types: ['techdocs'], @@ -81,7 +91,12 @@ describe('SearchResultList', () => { await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultList query={{ types: ['techdocs'], @@ -106,7 +121,12 @@ describe('SearchResultList', () => { query.mockReturnValueOnce(new Promise(() => {})); await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultList query={{ types: ['techdocs'], @@ -125,7 +145,12 @@ describe('SearchResultList', () => { query.mockResolvedValueOnce({ results: [] }); await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultList query={{ types: ['techdocs'] }} disableRenderingWithNoResults @@ -143,7 +168,12 @@ describe('SearchResultList', () => { query.mockResolvedValueOnce({ results: [] }); await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultList query={{ types: ['techdocs'] }} noResultsComponent="No results were found" @@ -161,7 +191,12 @@ describe('SearchResultList', () => { query.mockRejectedValueOnce(new Error()); await renderWithEffects( wrapInTestApp( - <TestApiProvider apis={[[searchApiRef, searchApiMock]]}> + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > <SearchResultList query={{ types: ['techdocs'], @@ -179,4 +214,44 @@ describe('SearchResultList', () => { ).toBeInTheDocument(); }); }); + + it('should render search results using list item extensions', async () => { + query.mockResolvedValueOnce({ + results, + }); + + const SearchResultListItemExtension = createPlugin({ + id: 'plugin', + }).provide( + createSearchResultListItemExtension({ + name: 'SearchResultListItemExtension', + component: async () => props => <>Result: {props.result?.title}</>, + }), + ); + + await renderWithEffects( + wrapInTestApp( + <TestApiProvider + apis={[ + [searchApiRef, searchApiMock], + [analyticsApiRef, analyticsApiMock], + ]} + > + <SearchResultList + query={{ + types: ['techdocs'], + }} + > + <SearchResultListItemExtension /> + </SearchResultList> + </TestApiProvider>, + ), + ); + + await waitFor(() => { + expect(screen.getByText('Result: Search Result 1')).toBeInTheDocument(); + }); + + expect(screen.getByText('Result: Search Result 2')).toBeInTheDocument(); + }); }); diff --git a/plugins/search-react/src/components/SearchResultList/SearchResultList.tsx b/plugins/search-react/src/components/SearchResultList/SearchResultList.tsx index 4279e8936a..1a539b74a6 100644 --- a/plugins/search-react/src/components/SearchResultList/SearchResultList.tsx +++ b/plugins/search-react/src/components/SearchResultList/SearchResultList.tsx @@ -16,24 +16,34 @@ import React, { ReactNode } from 'react'; -import { List, ListItem, ListProps } from '@material-ui/core'; +import { List, ListProps } from '@material-ui/core'; import { - EmptyState, Progress, + EmptyState, ResponseErrorPanel, } from '@backstage/core-components'; import { AnalyticsContext } from '@backstage/core-plugin-api'; -import { SearchQuery, SearchResult } from '@backstage/plugin-search-common'; +import { SearchResult } from '@backstage/plugin-search-common'; + +import { useSearchResultListItemExtensions } from '../../extensions'; import { DefaultResultListItem } from '../DefaultResultListItem'; -import { SearchResultState } from '../SearchResult'; +import { SearchResultState, SearchResultStateProps } from '../SearchResult'; /** * Props for {@link SearchResultListLayout} * @public */ export type SearchResultListLayoutProps = ListProps & { + /** + * If defined, will render a default error panel. + */ + error?: Error; + /** + * If defined, will render a default loading progress. + */ + loading?: boolean; /** * Search results to be rendered as a list. */ @@ -46,18 +56,14 @@ export type SearchResultListLayoutProps = ListProps & { index: number, array: SearchResult[], ) => JSX.Element | null; - /** - * If defined, will render a default error panel. - */ - error?: Error; - /** - * If defined, will render a default loading progress. - */ - loading?: boolean; /** * Optional component to render when no results. Default to <EmptyState /> component. */ noResultsComponent?: ReactNode; + /** + * Optional property to provide if component should not render the component when no results are found. + */ + disableRenderingWithNoResults?: boolean; }; /** @@ -67,8 +73,8 @@ export type SearchResultListLayoutProps = ListProps & { */ export const SearchResultListLayout = (props: SearchResultListLayoutProps) => { const { - loading, error, + loading, resultItems, renderResultItem = resultItem => ( <DefaultResultListItem @@ -76,48 +82,39 @@ export const SearchResultListLayout = (props: SearchResultListLayoutProps) => { result={resultItem.document} /> ), - noResultsComponent = ( + disableRenderingWithNoResults, + noResultsComponent = disableRenderingWithNoResults ? null : ( <EmptyState missing="data" title="Sorry, no results were found" /> ), ...rest } = props; - return ( - <List {...rest}> - {loading ? <Progress /> : null} - {!loading && error ? ( - <ResponseErrorPanel - title="Error encountered while fetching search results" - error={error} - /> - ) : null} - {!loading && !error && resultItems?.length - ? resultItems.map(renderResultItem) - : null} - {!loading && !error && !resultItems?.length ? ( - <ListItem>{noResultsComponent}</ListItem> - ) : null} - </List> - ); + if (loading) { + return <Progress />; + } + + if (error) { + return ( + <ResponseErrorPanel + title="Error encountered while fetching search results" + error={error} + /> + ); + } + + if (!resultItems?.length) { + return <>{noResultsComponent}</>; + } + + return <List {...rest}>{resultItems.map(renderResultItem)}</List>; }; /** * Props for {@link SearchResultList}. * @public */ -export type SearchResultListProps = Omit< - SearchResultListLayoutProps, - 'loading' | 'error' | 'resultItems' -> & { - /** - * A search query used for requesting the results to be listed. - */ - query: Partial<SearchQuery>; - /** - * Optional property to provide if component should not render the component when no results are found. - */ - disableRenderingWithNoResults?: boolean; -}; +export type SearchResultListProps = Pick<SearchResultStateProps, 'query'> & + Omit<SearchResultListLayoutProps, 'loading' | 'error' | 'resultItems'>; /** * Given a query, search for results and render them as a list. @@ -125,7 +122,9 @@ export type SearchResultListProps = Omit< * @public */ export const SearchResultList = (props: SearchResultListProps) => { - const { query, disableRenderingWithNoResults, ...rest } = props; + const { query, renderResultItem, children, ...rest } = props; + + const defaultRenderResultItem = useSearchResultListItemExtensions(children); return ( <AnalyticsContext @@ -135,20 +134,15 @@ export const SearchResultList = (props: SearchResultListProps) => { }} > <SearchResultState query={query}> - {({ loading, error, value }) => { - if (!value?.results?.length && disableRenderingWithNoResults) { - return null; - } - - return ( - <SearchResultListLayout - {...rest} - loading={loading} - error={error} - resultItems={value?.results} - /> - ); - }} + {({ loading, error, value }) => ( + <SearchResultListLayout + {...rest} + error={error} + loading={loading} + resultItems={value?.results} + renderResultItem={renderResultItem ?? defaultRenderResultItem} + /> + )} </SearchResultState> </AnalyticsContext> ); diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 5f68905407..c85714fa46 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -18,9 +18,9 @@ export * from './HighlightedSearchResultText'; export * from './SearchBar'; export * from './SearchAutocomplete'; export * from './SearchFilter'; -export * from './SearchResult'; -export * from './SearchResultPager'; export * from './SearchPagination'; +export * from './SearchResult'; export * from './SearchResultList'; export * from './SearchResultGroup'; +export * from './SearchResultPager'; export * from './DefaultResultListItem'; diff --git a/plugins/search-react/src/extensions.test.tsx b/plugins/search-react/src/extensions.test.tsx new file mode 100644 index 0000000000..7e5070c738 --- /dev/null +++ b/plugins/search-react/src/extensions.test.tsx @@ -0,0 +1,207 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { ListItemText } from '@material-ui/core'; + +import { + wrapInTestApp, + renderWithEffects, + TestApiProvider, + MockAnalyticsApi, +} from '@backstage/test-utils'; +import { + createPlugin, + BackstagePlugin, + analyticsApiRef, +} from '@backstage/core-plugin-api'; +import { SearchResult, SearchDocument } from '@backstage/plugin-search-common'; + +import { + SearchResultListItemExtensions, + createSearchResultListItemExtension, + SearchResultListItemExtensionOptions, +} from './extensions'; + +const analyticsApiMock = new MockAnalyticsApi(); + +const results = [ + { + type: 'explore', + document: { + location: 'search/search-result1', + title: 'Search Result 1', + text: 'Some text from the search result 1', + }, + }, + { + type: 'techdocs', + document: { + location: 'search/search-result2', + title: 'Search Result 2', + text: 'Some text from the search result 2', + }, + }, +]; + +const createExtension = ( + plugin: BackstagePlugin, + options: Partial< + Omit< + SearchResultListItemExtensionOptions< + (props: { result?: SearchDocument }) => JSX.Element | null + >, + 'name' + > + > = {}, +) => { + const { + predicate, + component = async () => (props: { result?: SearchDocument }) => + ( + <> + <ListItemText primary="Default" secondary={props.result?.title} /> + </> + ), + } = options; + return plugin.provide( + createSearchResultListItemExtension({ + predicate, + component, + name: 'TestSearchResultItemExtension', + }), + ); +}; + +describe('extensions', () => { + it('renders without exploding', async () => { + await renderWithEffects( + wrapInTestApp( + <TestApiProvider apis={[[analyticsApiRef, analyticsApiMock]]}> + <SearchResultListItemExtensions results={results} /> + </TestApiProvider>, + ), + ); + + expect(screen.getByText('Search Result 1')).toBeInTheDocument(); + expect( + screen.getByText('Some text from the search result 1'), + ).toBeInTheDocument(); + + expect(screen.getByText('Search Result 2')).toBeInTheDocument(); + expect( + screen.getByText('Some text from the search result 2'), + ).toBeInTheDocument(); + }); + + it('capture results discovery events', async () => { + await renderWithEffects( + wrapInTestApp( + <TestApiProvider apis={[[analyticsApiRef, analyticsApiMock]]}> + <SearchResultListItemExtensions results={results} /> + </TestApiProvider>, + ), + ); + + await userEvent.click( + screen.getByRole('link', { name: /Search Result 1/ }), + ); + + expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + action: 'discover', + subject: 'Search Result 1', + context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, + attributes: { to: 'search/search-result1' }, + }); + }); + + it('Could be used as simple components', async () => { + const plugin = createPlugin({ id: 'plugin' }); + const DefaultSearchResultListItemExtension = createExtension(plugin); + + await renderWithEffects( + wrapInTestApp( + <TestApiProvider apis={[[analyticsApiRef, analyticsApiMock]]}> + <DefaultSearchResultListItemExtension result={results[0].document} /> + </TestApiProvider>, + ), + ); + + expect(screen.getByText('Default')).toBeInTheDocument(); + expect(screen.getByText('Search Result 1')).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('listitem')); + + expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + action: 'discover', + subject: 'Search Result 1', + context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, + attributes: { to: 'search/search-result1' }, + }); + }); + + it('use default options for rendering results', async () => { + const plugin = createPlugin({ id: 'plugin' }); + const DefaultSearchResultListItemExtension = createExtension(plugin); + + await renderWithEffects( + wrapInTestApp( + <TestApiProvider apis={[[analyticsApiRef, analyticsApiMock]]}> + <SearchResultListItemExtensions results={results}> + <DefaultSearchResultListItemExtension /> + </SearchResultListItemExtensions> + </TestApiProvider>, + ), + ); + + expect(screen.getAllByText('Default')).toHaveLength(2); + expect(screen.getByText('Search Result 1')).toBeInTheDocument(); + expect(screen.getByText('Search Result 2')).toBeInTheDocument(); + }); + + it('use custom options for rendering results', async () => { + const plugin = createPlugin({ id: 'plugin' }); + const DefaultSearchResultListItemExtension = createExtension(plugin); + const ExploreSearchResultListItemExtension = createExtension(plugin, { + predicate: (result: SearchResult) => result.type === 'explore', + component: async () => (props: { result?: SearchDocument }) => + ( + <> + <ListItemText primary="Explore" secondary={props.result?.title} /> + </> + ), + }); + + await renderWithEffects( + wrapInTestApp( + <TestApiProvider apis={[[analyticsApiRef, analyticsApiMock]]}> + <SearchResultListItemExtensions results={results}> + <ExploreSearchResultListItemExtension /> + <DefaultSearchResultListItemExtension /> + </SearchResultListItemExtensions> + </TestApiProvider>, + ), + ); + + expect(screen.getAllByText('Default')).toHaveLength(1); + expect(screen.getAllByText('Explore')).toHaveLength(1); + expect(screen.getByText('Search Result 1')).toBeInTheDocument(); + expect(screen.getByText('Search Result 2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/search-react/src/extensions.tsx b/plugins/search-react/src/extensions.tsx new file mode 100644 index 0000000000..6c854e24e0 --- /dev/null +++ b/plugins/search-react/src/extensions.tsx @@ -0,0 +1,251 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { + Fragment, + ReactNode, + PropsWithChildren, + isValidElement, + createElement, + cloneElement, + useCallback, +} from 'react'; + +import { + getComponentData, + useElementFilter, + Extension, + createReactExtension, + useAnalytics, +} from '@backstage/core-plugin-api'; +import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; + +import { ListItem, List, ListProps, ListItemProps } from '@material-ui/core'; + +import { DefaultResultListItem } from './components'; + +/** + * @internal + * Key for result extensions. + */ +const SEARCH_RESULT_LIST_ITEM_EXTENSION = + 'search.results.list.items.extensions.v1'; + +/** + * @internal + * Returns the first extension element found for a given result, and null otherwise. + * @param elements - All extension elements. + * @param result - The search result. + */ +const findSearchResultListItemExtensionElement = ( + elements: ReactNode[], + result: SearchResult, +) => { + for (const element of elements) { + if (!isValidElement(element)) continue; + const predicate = getComponentData<(result: SearchResult) => boolean>( + element, + SEARCH_RESULT_LIST_ITEM_EXTENSION, + ); + if (!predicate?.(result)) continue; + return cloneElement(element, { + rank: result.rank, + highlight: result.highlight, + result: result.document, + // Use props in situations where a consumer is manually rendering the extension + ...element.props, + }); + } + return null; +}; + +/** + * @public + * Extends props for any search result list item extension + */ +export type SearchResultListItemExtensionProps<Props extends {} = {}> = Props & + PropsWithChildren< + { + rank?: number; + result?: SearchDocument; + noTrack?: boolean; + } & Omit<ListItemProps, 'button'> + >; + +/** + * @internal + * Extends children with extension capabilities. + * @param props - see {@link SearchResultListItemExtensionProps}. + */ +const SearchResultListItemExtension = ( + props: SearchResultListItemExtensionProps, +) => { + const { + rank, + result, + noTrack, + children, + alignItems = 'flex-start', + ...rest + } = props; + const analytics = useAnalytics(); + + const handleClickCapture = useCallback(() => { + if (noTrack) return; + if (!result) return; + analytics.captureEvent('discover', result.title, { + attributes: { to: result.location }, + value: rank, + }); + }, [rank, result, noTrack, analytics]); + + return ( + <ListItem + divider + alignItems={alignItems} + onClickCapture={handleClickCapture} + {...rest} + > + {children} + </ListItem> + ); +}; + +/** + * @public + * Options for {@link createSearchResultListItemExtension}. + */ +export type SearchResultListItemExtensionOptions< + Component extends (props: any) => JSX.Element | null, +> = { + /** + * The extension name. + */ + name: string; + /** + * The extension component. + */ + component: () => Promise<Component>; + /** + * When an extension defines a predicate, it returns true if the result should be rendered by that extension. + * Defaults to a predicate that returns true, which means it renders all sorts of results. + */ + predicate?: (result: SearchResult) => boolean; +}; + +/** + * @public + * Creates a search result item extension. + * @param options - The extension options, see {@link SearchResultListItemExtensionOptions} for more details. + */ +export const createSearchResultListItemExtension = < + Component extends (props: any) => JSX.Element | null, +>( + options: SearchResultListItemExtensionOptions<Component>, +): Extension<Component> => { + const { name, component, predicate = () => true } = options; + + return createReactExtension<Component>({ + name, + component: { + lazy: () => + component().then( + type => + (props => ( + <SearchResultListItemExtension + rank={props.rank} + result={props.result} + noTrack={props.noTrack} + > + {createElement(type, props)} + </SearchResultListItemExtension> + )) as Component, + ), + }, + data: { + [SEARCH_RESULT_LIST_ITEM_EXTENSION]: predicate, + }, + }); +}; + +/** + * @public + * Returns a function that renders a result using extensions. + */ +export const useSearchResultListItemExtensions = (children: ReactNode) => { + const elements = useElementFilter( + children, + collection => { + return collection + .selectByComponentData({ + key: SEARCH_RESULT_LIST_ITEM_EXTENSION, + }) + .getElements(); + }, + [children], + ); + + return useCallback( + (result: SearchResult, key?: number) => { + const element = findSearchResultListItemExtensionElement( + elements, + result, + ); + + return ( + <Fragment key={key}> + {element ?? ( + <SearchResultListItemExtension + rank={result.rank} + result={result.document} + > + <DefaultResultListItem + rank={result.rank} + highlight={result.highlight} + result={result.document} + /> + </SearchResultListItemExtension> + )} + </Fragment> + ); + }, + [elements], + ); +}; + +/** + * @public + * Props for {@link SearchResultListItemExtensions} + */ +export type SearchResultListItemExtensionsProps = Omit<ListProps, 'results'> & { + /** + * Search result list. + */ + results: SearchResult[]; +}; + +/** + * @public + * Render results using search extensions. + * @param props - see {@link SearchResultListItemExtensionsProps} + */ +export const SearchResultListItemExtensions = ( + props: SearchResultListItemExtensionsProps, +) => { + const { results, children, ...rest } = props; + const render = useSearchResultListItemExtensions(children); + return <List {...rest}>{results.map(render)}</List>; +}; diff --git a/plugins/search-react/src/index.ts b/plugins/search-react/src/index.ts index 9813661f3e..8b234e3c05 100644 --- a/plugins/search-react/src/index.ts +++ b/plugins/search-react/src/index.ts @@ -22,6 +22,7 @@ export { searchApiRef, MockSearchApi } from './api'; export type { SearchApi } from './api'; +export * from './extensions'; export * from './components'; export { SearchContextProvider, diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index ea24b77523..974fcc4a5d 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,92 @@ # @backstage/plugin-search +## 1.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## 1.1.0 + +### Minor Changes + +- 0eaa579f89: Update `SearchModal` component to use `SearchResult` extensions. + +### Patch Changes + +- 26fb21aa8b: Implement a `showCounts` option to display result counts per type in `SearchType.Accordion` +- 66e2aab4c4: Updated colors for each tab used on search modal, to improve color contrast. Aria label added to tabs wrapper component. `disableRipple` property on the `Tab` component removed to improve keyboard navigation indicator. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.0-next.2 + +### Patch Changes + +- 66e2aab4c4: Updated colors for each tab used on search modal, to improve color contrast. Aria label added to tabs wrapper component. `disableRipple` property on the `Tab` component removed to improve keyboard navigation indicator. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + +## 1.1.0-next.1 + +### Minor Changes + +- 0eaa579f89: Update `SearchModal` component to use `SearchResult` extensions. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 1.0.7 ### Patch Changes diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index cdbdc4c095..09c12cd776 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -97,6 +97,7 @@ export type SearchTypeAccordionProps = { icon: JSX.Element; }>; defaultValue?: string; + showCounts?: boolean; }; // @public diff --git a/plugins/search/package.json b/plugins/search/package.json index 21b6631504..31b57878dd 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "1.0.7", + "version": "1.1.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -65,7 +65,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index b1a0b2cd45..c9621c5940 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -23,7 +23,6 @@ import { DialogTitle, Divider, Grid, - List, Paper, useTheme, } from '@material-ui/core'; @@ -31,7 +30,6 @@ import Typography from '@material-ui/core/Typography'; import LaunchIcon from '@material-ui/icons/Launch'; import { makeStyles } from '@material-ui/core/styles'; import { - DefaultResultListItem, SearchContextProvider, SearchBar, SearchResult, @@ -44,7 +42,7 @@ import { rootRouteRef } from '../../plugin'; /** * @public - **/ + */ export interface SearchModalChildrenProps { /** * A function that should be invoked when navigating away from the modal. @@ -54,7 +52,7 @@ export interface SearchModalChildrenProps { /** * @public - **/ + */ export interface SearchModalProps { /** * If true, it renders the modal. @@ -151,27 +149,10 @@ export const Modal = ({ toggleModal }: SearchModalProps) => { </Grid> </Grid> <Divider /> - <SearchResult> - {({ results }) => ( - <List> - {results.map(({ document, highlight }) => ( - <div - role="button" - tabIndex={0} - key={`${document.location}-btn`} - onClick={handleSearchResultClick} - onKeyDown={handleSearchResultClick} - > - <DefaultResultListItem - key={document.location} - result={document} - highlight={highlight} - /> - </div> - ))} - </List> - )} - </SearchResult> + <SearchResult + onClick={handleSearchResultClick} + onKeyDown={handleSearchResultClick} + /> </DialogContent> <DialogActions className={classes.dialogActionsContainer}> <Grid container direction="row"> diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index da1cae46b9..e0acee025a 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { TestApiProvider } from '@backstage/test-utils'; -import { act, render } from '@testing-library/react'; +import { act, render, waitFor } from '@testing-library/react'; import user from '@testing-library/user-event'; import { searchApiRef, @@ -34,6 +34,8 @@ jest.mock('@backstage/plugin-search-react', () => ({ setTypes: (types: any) => setTypesMock(types), pageCursor: '', setPageCursor: (pageCursor: any) => setPageCursorMock(pageCursor), + term: 'abc', + filters: { foo: 'bar' }, }), })); @@ -48,7 +50,7 @@ describe('SearchType.Accordion', () => { }; beforeEach(() => { - query.mockResolvedValue({ results: [] }); + query.mockResolvedValue({ results: [], numberOfResults: 1234 }); }); const Wrapper = ({ children }: { children: React.ReactNode }) => { @@ -132,4 +134,34 @@ describe('SearchType.Accordion', () => { expect(queryByText('Collapse')).not.toBeInTheDocument(); }); + + it('should show result counts if enabled', async () => { + const { getAllByText } = render( + <Wrapper> + <SearchType.Accordion + name={expectedLabel} + showCounts + types={[expectedType]} + /> + </Wrapper>, + ); + + expect(query).toHaveBeenCalledWith({ + term: 'abc', + types: [], + filters: { foo: 'bar' }, + pageLimit: 0, + }); + expect(query).toHaveBeenCalledWith({ + term: 'abc', + types: [expectedType.value], + filters: {}, + pageLimit: 0, + }); + await waitFor(() => { + const countLabels = getAllByText('1234 results'); + expect(countLabels.length).toEqual(2); + expect(countLabels[0]).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx index 49f5cf74ca..67785b89cb 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx @@ -15,7 +15,8 @@ */ import React, { cloneElement, Fragment, useEffect, useState } from 'react'; -import { useSearch } from '@backstage/plugin-search-react'; +import { useApi } from '@backstage/core-plugin-api'; +import { searchApiRef, useSearch } from '@backstage/plugin-search-react'; import { Accordion, AccordionSummary, @@ -32,6 +33,7 @@ import { } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import AllIcon from '@material-ui/icons/FontDownload'; +import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles(theme => ({ card: { @@ -81,13 +83,15 @@ export type SearchTypeAccordionProps = { icon: JSX.Element; }>; defaultValue?: string; + showCounts?: boolean; }; export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { const classes = useStyles(); - const { setPageCursor, setTypes, types } = useSearch(); + const { filters, setPageCursor, setTypes, term, types } = useSearch(); + const searchApi = useApi(searchApiRef); const [expanded, setExpanded] = useState(true); - const { defaultValue, name, types: givenTypes } = props; + const { defaultValue, name, showCounts, types: givenTypes } = props; const toggleExpanded = () => setExpanded(prevState => !prevState); const handleClick = (type: string) => { @@ -116,6 +120,37 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { ]; const selected = types[0] || ''; + const { value: resultCounts } = useAsync(async () => { + if (!showCounts) { + return {}; + } + + const counts = await Promise.all( + definedTypes + .map(t => t.value) + .map(async type => { + const { numberOfResults } = await searchApi.query({ + term, + types: type ? [type] : [], + filters: + types.includes(type) || (!types.length && !type) ? filters : {}, + pageLimit: 0, + }); + + return [ + type, + numberOfResults !== undefined + ? `${ + numberOfResults >= 10000 ? `>10000` : numberOfResults + } results` + : ' -- ', + ]; + }), + ); + + return Object.fromEntries(counts); + }, [filters, showCounts, term, types]); + return ( <Card className={classes.card}> <CardHeader title={name} titleTypographyProps={{ variant: 'overline' }} /> @@ -161,7 +196,10 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { className: classes.listItemIcon, })} </ListItemIcon> - <ListItemText primary={type.name} /> + <ListItemText + primary={type.name} + secondary={resultCounts && resultCounts[type.value]} + /> </ListItem> </Fragment> ))} diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.tsx index fc0380e23a..57a311b00c 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.tsx @@ -22,13 +22,12 @@ import { makeStyles, Tab, Tabs } from '@material-ui/core'; const useStyles = makeStyles((theme: BackstageTheme) => ({ tabs: { borderBottom: `1px solid ${theme.palette.textVerySubtle}`, - padding: theme.spacing(0, 4), }, tab: { height: '50px', fontWeight: theme.typography.fontWeightBold, fontSize: theme.typography.pxToRem(13), - color: theme.palette.textSubtle, + color: theme.palette.text.primary, minWidth: '130px', }, })); @@ -72,6 +71,7 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => { return ( <Tabs + aria-label="List of search types tabs" className={classes.tabs} indicatorColor="primary" value={types.length === 0 ? '' : types[0]} @@ -81,7 +81,6 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => { <Tab key={idx} className={classes.tab} - disableRipple label={type.name} value={type.value} /> diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 8e1db7c2b8..66b032c83c 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -47,7 +47,7 @@ export type { SidebarSearchModalProps } from './components/SidebarSearchModal'; export { HomePageSearchBar, SearchPage, + SidebarSearchModal, searchPlugin as plugin, searchPlugin, - SidebarSearchModal, } from './plugin'; diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 9a232f5be1..1f23a87326 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-sentry +## 0.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.5.0 + +### Minor Changes + +- 6a8d6b9a0c: Add option to filter issues based on the time it was triggered + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.5.0-next.2 + +### Minor Changes + +- 6a8d6b9a0c: Add option to filter issues based on the time it was triggered + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.4.8-next.1 + +### Patch Changes + +- 85b04f659a: Internal refactor to not use deprecated `substr` +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.4.7 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 14a9f1bd1c..7d385260f4 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.4.7", + "version": "0.5.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -62,7 +62,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx index 015cf96ed1..14e0213bfa 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx @@ -22,7 +22,7 @@ import { BackstageTheme } from '@backstage/theme'; import { Link } from '@backstage/core-components'; function stripText(text: string, maxLength: number) { - return text.length > maxLength ? `${text.substr(0, maxLength)}...` : text; + return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text; } const useStyles = makeStyles<BackstageTheme>(theme => ({ root: { diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx index 647208f141..5bf6b1e92b 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx @@ -19,6 +19,7 @@ import SentryIssuesTable from './SentryIssuesTable'; import { SentryIssue } from '../../api'; import mockIssue from '../../api/mock/sentry-issue-mock.json'; import { renderInTestApp } from '@backstage/test-utils'; +import { DateTime } from 'luxon'; describe('SentryIssuesTable', () => { it('should render headers in a table', async () => { @@ -62,6 +63,7 @@ describe('SentryIssuesTable', () => { }, count: '101', userCount: 202, + lastSeen: DateTime.now().toISO(), }, ]; const table = await renderInTestApp( @@ -105,6 +107,6 @@ describe('SentryIssuesTable', () => { }} />, ); - expect(await table.findByText('Last 24h')).toBeInTheDocument(); + expect(await table.findByText('Stats for 24h')).toBeInTheDocument(); }); }); diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx index abe29efd7f..60ee6b7dbf 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -14,13 +14,19 @@ * limitations under the License. */ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { SentryIssue } from '../../api'; -import { DateTime } from 'luxon'; +import { DateTime, Duration } from 'luxon'; import { ErrorCell } from '../ErrorCell/ErrorCell'; import { ErrorGraph } from '../ErrorGraph/ErrorGraph'; import { Table, TableColumn } from '@backstage/core-components'; +import Select from '@material-ui/core/Select'; import { Options } from '@material-table/core'; +import { FormControl, Grid, MenuItem } from '@material-ui/core'; + +const ONE_DAY_IN_MILLIS = 86400000; +const SEVEN_DAYS_IN_MILLIS = ONE_DAY_IN_MILLIS * 7; +const FOURTEEN_DAYS_IN_MILLIS = ONE_DAY_IN_MILLIS * 14; const columns: TableColumn[] = [ { @@ -66,14 +72,55 @@ type SentryIssuesTableProps = { const SentryIssuesTable = (props: SentryIssuesTableProps) => { const { sentryIssues, statsFor, tableOptions } = props; + const [selected, setSelected] = useState(ONE_DAY_IN_MILLIS); + + const filterByDate = useCallback((issue, selectedFilter) => { + return ( + DateTime.fromISO(issue.lastSeen) > + DateTime.now().minus(Duration.fromMillis(selectedFilter)) + ); + }, []); + const [filteredIssues, setFilteredIssues] = useState( + sentryIssues.filter(i => filterByDate(i, selected)), + ); + + const handleFilterChange = ( + event: React.ChangeEvent<{ name?: string; value: unknown }>, + ) => { + const item = event.target.value; + if (typeof item === 'number') { + setSelected(item); + if (item === Number.NEGATIVE_INFINITY) { + setFilteredIssues(sentryIssues); + return; + } + setFilteredIssues(sentryIssues.filter(i => filterByDate(i, item))); + } + }; return ( - <Table - columns={columns} - options={tableOptions} - title="Sentry issues" - subtitle={statsFor ? `Last ${statsFor}` : undefined} - data={sentryIssues} - /> + <> + <Table + columns={columns} + options={tableOptions} + title={ + <Grid container> + <Grid item>Sentry Issues</Grid> + <Grid item> + <FormControl variant="outlined" size="small"> + <Select value={selected} onChange={handleFilterChange}> + <MenuItem value={ONE_DAY_IN_MILLIS}>24H</MenuItem> + <MenuItem value={SEVEN_DAYS_IN_MILLIS}>7D</MenuItem> + <MenuItem value={FOURTEEN_DAYS_IN_MILLIS}>14D</MenuItem> + <MenuItem value={Number.NEGATIVE_INFINITY}>All</MenuItem> + </Select> + </FormControl> + </Grid> + </Grid> + } + subtitle={statsFor ? `Stats for ${statsFor}` : undefined} + data={filteredIssues} + /> + </> ); }; diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index f301ff2692..ed4df4535b 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-shortcuts +## 0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + +## 0.3.7 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/types@1.0.2 + +## 0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## 0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + ## 0.3.6 ### Patch Changes diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md index d7f8f19aec..cd79901656 100644 --- a/plugins/shortcuts/README.md +++ b/plugins/shortcuts/README.md @@ -7,7 +7,8 @@ The shortcuts plugin allows a user to have easy access to pages within a Backsta ### Install the package: ```bash -yarn add @backstage/plugin-shortcuts +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-shortcuts ``` ### Register plugin: diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 22e5abe6ad..0209e76ec7 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.3.6", + "version": "0.3.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -50,7 +50,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/sonarqube-backend/CHANGELOG.md b/plugins/sonarqube-backend/CHANGELOG.md index 57a32877e6..2543e6ca49 100644 --- a/plugins/sonarqube-backend/CHANGELOG.md +++ b/plugins/sonarqube-backend/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-sonarqube-backend +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.1.5 ### Patch Changes diff --git a/plugins/sonarqube-backend/package.json b/plugins/sonarqube-backend/package.json index 4f1e560bd0..6fc9a7080a 100644 --- a/plugins/sonarqube-backend/package.json +++ b/plugins/sonarqube-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube-backend", - "version": "0.1.5", + "version": "0.1.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,7 +36,7 @@ "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/supertest": "^2.0.12", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.2.4" }, "files": [ diff --git a/plugins/sonarqube-react/CHANGELOG.md b/plugins/sonarqube-react/CHANGELOG.md index 5368c648dc..7372052227 100644 --- a/plugins/sonarqube-react/CHANGELOG.md +++ b/plugins/sonarqube-react/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-sonarqube-react +## 0.1.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- 8ae65a0bc1: Moved imports from `/alpha` to main public exports. +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + +## 0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.1 ### Patch Changes diff --git a/plugins/sonarqube-react/alpha-api-report.md b/plugins/sonarqube-react/alpha-api-report.md new file mode 100644 index 0000000000..4fad469d12 --- /dev/null +++ b/plugins/sonarqube-react/alpha-api-report.md @@ -0,0 +1,65 @@ +## API Report File for "@backstage/plugin-sonarqube-react" + +> 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 { Entity } from '@backstage/catalog-model'; + +// @public (undocumented) +export interface FindingSummary { + // (undocumented) + getComponentMeasuresUrl: SonarUrlProcessorFunc; + // (undocumented) + getIssuesUrl: SonarUrlProcessorFunc; + // (undocumented) + getSecurityHotspotsUrl: () => string; + // (undocumented) + lastAnalysis: string; + // (undocumented) + metrics: Metrics; + // (undocumented) + projectUrl: string; +} + +// @public (undocumented) +export type MetricKey = + | 'alert_status' + | 'bugs' + | 'reliability_rating' + | 'vulnerabilities' + | 'security_rating' + | 'code_smells' + | 'sqale_rating' + | 'security_hotspots_reviewed' + | 'security_review_rating' + | 'coverage' + | 'duplicated_lines_density'; + +// @public +export type Metrics = { + [key in MetricKey]: string | undefined; +}; + +// @public (undocumented) +export type SonarQubeApi = { + getFindingSummary(options: { + componentKey?: string; + projectInstance?: string; + }): Promise<FindingSummary | undefined>; +}; + +// @public (undocumented) +export const sonarQubeApiRef: ApiRef<SonarQubeApi>; + +// @public (undocumented) +export type SonarUrlProcessorFunc = (identifier: string) => string; + +// @public +export const useProjectInfo: (entity: Entity) => { + projectInstance: string | undefined; + projectKey: string | undefined; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/sonarqube-react/api-report.md b/plugins/sonarqube-react/api-report.md index be84c45cce..6ffa98915b 100644 --- a/plugins/sonarqube-react/api-report.md +++ b/plugins/sonarqube-react/api-report.md @@ -6,7 +6,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -// @alpha (undocumented) +// @public (undocumented) export interface FindingSummary { // (undocumented) getComponentMeasuresUrl: SonarUrlProcessorFunc; @@ -25,7 +25,7 @@ export interface FindingSummary { // @public (undocumented) export const isSonarQubeAvailable: (entity: Entity) => boolean; -// @alpha (undocumented) +// @public (undocumented) export type MetricKey = | 'alert_status' | 'bugs' @@ -39,7 +39,7 @@ export type MetricKey = | 'coverage' | 'duplicated_lines_density'; -// @alpha +// @public export type Metrics = { [key in MetricKey]: string | undefined; }; @@ -47,7 +47,7 @@ export type Metrics = { // @public (undocumented) export const SONARQUBE_PROJECT_KEY_ANNOTATION = 'sonarqube.org/project-key'; -// @alpha (undocumented) +// @public (undocumented) export type SonarQubeApi = { getFindingSummary(options: { componentKey?: string; @@ -55,13 +55,13 @@ export type SonarQubeApi = { }): Promise<FindingSummary | undefined>; }; -// @alpha (undocumented) +// @public (undocumented) export const sonarQubeApiRef: ApiRef<SonarQubeApi>; -// @alpha (undocumented) +// @public (undocumented) export type SonarUrlProcessorFunc = (identifier: string) => string; -// @alpha +// @public export const useProjectInfo: (entity: Entity) => { projectInstance: string | undefined; projectKey: string | undefined; diff --git a/plugins/sonarqube-react/package.json b/plugins/sonarqube-react/package.json index 9332cc5548..9a567040cb 100644 --- a/plugins/sonarqube-react/package.json +++ b/plugins/sonarqube-react/package.json @@ -1,14 +1,22 @@ { "name": "@backstage/plugin-sonarqube-react", - "version": "0.1.1", + "version": "0.1.4-next.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", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "web-library" @@ -23,7 +31,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", diff --git a/plugins/sonarqube-react/src/api/SonarQubeApi.ts b/plugins/sonarqube-react/src/api/SonarQubeApi.ts index ee6b1bb6a3..13517d97fe 100644 --- a/plugins/sonarqube-react/src/api/SonarQubeApi.ts +++ b/plugins/sonarqube-react/src/api/SonarQubeApi.ts @@ -16,7 +16,7 @@ import { createApiRef } from '@backstage/core-plugin-api'; -/** @alpha */ +/** @public */ export type MetricKey = // alert status | 'alert_status' @@ -43,11 +43,11 @@ export type MetricKey = // duplicated lines | 'duplicated_lines_density'; -/** @alpha */ +/** @public */ export type SonarUrlProcessorFunc = (identifier: string) => string; /** - * @alpha + * @public * * Define a type to make sure that all metrics are used */ @@ -55,7 +55,7 @@ export type Metrics = { [key in MetricKey]: string | undefined; }; -/** @alpha */ +/** @public */ export interface FindingSummary { lastAnalysis: string; metrics: Metrics; @@ -65,12 +65,12 @@ export interface FindingSummary { getSecurityHotspotsUrl: () => string; } -/** @alpha */ +/** @public */ export const sonarQubeApiRef = createApiRef<SonarQubeApi>({ id: 'plugin.sonarqube.service', }); -/** @alpha */ +/** @public */ export type SonarQubeApi = { getFindingSummary(options: { componentKey?: string; diff --git a/plugins/sonarqube-react/src/hooks/useProjectInfo.ts b/plugins/sonarqube-react/src/hooks/useProjectInfo.ts index 119e568e46..3336bd1805 100644 --- a/plugins/sonarqube-react/src/hooks/useProjectInfo.ts +++ b/plugins/sonarqube-react/src/hooks/useProjectInfo.ts @@ -25,7 +25,7 @@ export const SONARQUBE_PROJECT_INSTANCE_SEPARATOR = '/'; * * If part or all info are not found, they will default to undefined * - * @alpha + * @public * @param entity - entity to find the sonarqube information from. * @returns a ProjectInfo properly populated. */ diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index e2e76c2b89..7235835af6 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-sonarqube +## 0.6.5-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- 8ae65a0bc1: Moved imports from `/alpha` to main public exports. +- d3d624e8e8: Show a more appropriate icon if there are no code smells and/or vulnerabilities. +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/plugin-sonarqube-react@0.1.4-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.6.3 + +### Patch Changes + +- 6310eacc11: Additional export added in order to bind SonarQubeClient to its apiref +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-sonarqube-react@0.1.2 + +## 0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-sonarqube-react@0.1.2-next.1 + +## 0.6.3-next.1 + +### Patch Changes + +- 6310eacc11: Additional export added in order to bind SonarQubeClient to its apiref +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-sonarqube-react@0.1.2-next.0 + +## 0.6.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-sonarqube-react@0.1.2-next.0 + ## 0.6.2 ### Patch Changes diff --git a/plugins/sonarqube/api-report.md b/plugins/sonarqube/api-report.md index 31e98a0f4a..db3f38e31d 100644 --- a/plugins/sonarqube/api-report.md +++ b/plugins/sonarqube/api-report.md @@ -6,8 +6,12 @@ /// <reference types="react" /> import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { FindingSummary } from '@backstage/plugin-sonarqube-react'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { SonarQubeApi } from '@backstage/plugin-sonarqube-react'; // @public (undocumented) export type DuplicationRating = { @@ -38,6 +42,29 @@ export const SonarQubeCard: (props: { duplicationRatings?: DuplicationRating[]; }) => JSX.Element; +// @public (undocumented) +export class SonarQubeClient implements SonarQubeApi { + constructor({ + discoveryApi, + identityApi, + }: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }); + // (undocumented) + discoveryApi: DiscoveryApi; + // (undocumented) + getFindingSummary({ + componentKey, + projectInstance, + }?: { + componentKey?: string; + projectInstance?: string; + }): Promise<FindingSummary | undefined>; + // (undocumented) + identityApi: IdentityApi; +} + // @public (undocumented) export type SonarQubeContentPageProps = { title?: string; diff --git a/plugins/sonarqube/dev/index.tsx b/plugins/sonarqube/dev/index.tsx index a34c4d4e82..8d38c7314a 100644 --- a/plugins/sonarqube/dev/index.tsx +++ b/plugins/sonarqube/dev/index.tsx @@ -22,10 +22,10 @@ import { EntitySonarQubeCard, sonarQubePlugin } from '../src'; import { Content, Header, Page } from '@backstage/core-components'; import { FindingSummary, - SONARQUBE_PROJECT_KEY_ANNOTATION, SonarQubeApi, sonarQubeApiRef, } from '@backstage/plugin-sonarqube-react'; +import { SONARQUBE_PROJECT_KEY_ANNOTATION } from '@backstage/plugin-sonarqube-react'; const entity = (name?: string) => ({ diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index af04494906..ef14f6da58 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,14 +1,15 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.6.2", + "version": "0.6.5-next.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" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "frontend-plugin" @@ -25,7 +26,7 @@ "sonarcloud" ], "scripts": { - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "start": "backstage-cli package start", "lint": "backstage-cli package lint", "test": "backstage-cli package test", @@ -60,10 +61,11 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", + "alpha", "config.d.ts" ], "configSchema": "config.d.ts" diff --git a/plugins/sonarqube/src/api/SonarQubeClient.ts b/plugins/sonarqube/src/api/SonarQubeClient.ts index cf893ccef5..14c7936e92 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.ts @@ -23,6 +23,7 @@ import { import { InstanceUrlWrapper, FindingsWrapper } from './types'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +/** @public */ export class SonarQubeClient implements SonarQubeApi { discoveryApi: DiscoveryApi; identityApi: IdentityApi; diff --git a/plugins/sonarqube/src/api/types.ts b/plugins/sonarqube/src/api/types.ts index 470d05903a..29b7e7ca54 100644 --- a/plugins/sonarqube/src/api/types.ts +++ b/plugins/sonarqube/src/api/types.ts @@ -14,8 +14,10 @@ * limitations under the License. */ -import { MetricKey as NonDeprecatedMetricKey } from '@backstage/plugin-sonarqube-react'; -import { SonarUrlProcessorFunc as NonDeprecatedSonarUrlProcessorFunc } from '@backstage/plugin-sonarqube-react'; +import { + MetricKey as NonDeprecatedMetricKey, + SonarUrlProcessorFunc as NonDeprecatedSonarUrlProcessorFunc, +} from '@backstage/plugin-sonarqube-react'; export interface InstanceUrlWrapper { instanceUrl: string; diff --git a/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx index 900c8d082c..932641e48d 100644 --- a/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx +++ b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx @@ -16,16 +16,18 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import { - SONARQUBE_PROJECT_KEY_ANNOTATION, sonarQubeApiRef, useProjectInfo, } from '@backstage/plugin-sonarqube-react'; +import { SONARQUBE_PROJECT_KEY_ANNOTATION } from '@backstage/plugin-sonarqube-react'; import { Chip, Grid } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import BugReport from '@material-ui/icons/BugReport'; +import Lock from '@material-ui/icons/Lock'; import LockOpen from '@material-ui/icons/LockOpen'; import Security from '@material-ui/icons/Security'; import SentimentVeryDissatisfied from '@material-ui/icons/SentimentVeryDissatisfied'; +import SentimentVerySatisfied from '@material-ui/icons/SentimentVerySatisfied'; import React, { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { Percentage } from './Percentage'; @@ -200,14 +202,26 @@ export const SonarQubeCard = (props: { rightSlot={<Rating rating={value.metrics.reliability_rating} />} /> <RatingCard - titleIcon={<LockOpen />} + titleIcon={ + value.metrics.vulnerabilities === '0' ? ( + <Lock /> + ) : ( + <LockOpen /> + ) + } title="Vulnerabilities" link={value.getIssuesUrl('VULNERABILITY')} leftSlot={<Value value={value.metrics.vulnerabilities} />} rightSlot={<Rating rating={value.metrics.security_rating} />} /> <RatingCard - titleIcon={<SentimentVeryDissatisfied />} + titleIcon={ + value.metrics.code_smells === '0' ? ( + <SentimentVerySatisfied /> + ) : ( + <SentimentVeryDissatisfied /> + ) + } title="Code Smells" link={value.getIssuesUrl('CODE_SMELL')} leftSlot={<Value value={value.metrics.code_smells} />} diff --git a/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx index 59258e5312..0bf5f04640 100644 --- a/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx +++ b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx @@ -18,10 +18,10 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { - isSonarQubeAvailable, - SONARQUBE_PROJECT_KEY_ANNOTATION, SonarQubeApi, sonarQubeApiRef, + isSonarQubeAvailable, + SONARQUBE_PROJECT_KEY_ANNOTATION, } from '@backstage/plugin-sonarqube-react'; const Providers = ({ diff --git a/plugins/sonarqube/src/index.ts b/plugins/sonarqube/src/index.ts index 30f6351d4d..93648caeb5 100644 --- a/plugins/sonarqube/src/index.ts +++ b/plugins/sonarqube/src/index.ts @@ -21,5 +21,6 @@ * @packageDocumentation */ +export * from './api'; export * from './components'; export * from './plugin'; diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index 05e2fa7794..abf9a2cca9 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-splunk-on-call +## 0.4.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.4.4 + +### Patch Changes + +- 3598136ac7: Refactor plugin Card component to not rerender contents unnecessarily. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + +## 0.4.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.4.4-next.0 + +### Patch Changes + +- 3598136ac7: Refactor plugin Card component to not rerender contents unnecessarily. +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.4.3 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 3b8aebcd71..2703a95871 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.4.3", + "version": "0.4.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -60,7 +60,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "configSchema": "config.d.ts", "files": [ diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 26521ea9ee..54784b1f4e 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -33,7 +33,7 @@ import { MissingApiKeyOrApiIdError } from './Errors'; import { EscalationPolicy } from './Escalation'; import { Incidents } from './Incident'; import { TriggerDialog } from './TriggerDialog'; -import { RoutingKey, Team, User } from './types'; +import { RoutingKey, User } from './types'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { @@ -219,37 +219,6 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { ); } - const Content = ({ - team, - routingKey, - usersHashMap, - }: { - team: Team | undefined; - routingKey: RoutingKey | undefined; - usersHashMap: any; - }) => { - const teamName = team?.name ?? ''; - - return ( - <> - <Incidents - readOnly={readOnly || false} - team={teamName} - refreshIncidents={refreshIncidents} - /> - {usersHashMap && team && ( - <EscalationPolicy team={teamName} users={usersHashMap} /> - )} - <TriggerDialog - routingKey={routingKey?.routingKey ?? teamName} - showDialog={showDialog} - handleDialog={handleDialog} - onIncidentCreated={handleRefresh} - /> - </> - ); - }; - const triggerLink: IconLinkVerticalProps = { label: 'Create Incident', onClick: handleDialog, @@ -267,30 +236,45 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { return ( <> - {teams.map((team, i) => ( - <Card key={i} className={classes.onCallCard}> - <CardHeader - title="Splunk On-Call" - subheader={[ - <Typography key="team_name"> - Team: {team && team.name ? team.name : ''} - </Typography>, - <HeaderIconLinkRow - key="incident_trigger" - links={!readOnly ? [serviceLink, triggerLink] : [serviceLink]} - />, - ]} - /> - <Divider /> - <CardContent> - <Content - team={team} - routingKey={entityData?.foundRoutingKey} - usersHashMap={entityData?.usersHashMap} + {teams.map((team, i) => { + const teamName = team?.name ?? ''; + return ( + <Card key={i} className={classes.onCallCard}> + <CardHeader + title="Splunk On-Call" + subheader={[ + <Typography key="team_name"> + Team: {team && team.name ? team.name : ''} + </Typography>, + <HeaderIconLinkRow + key="incident_trigger" + links={!readOnly ? [serviceLink, triggerLink] : [serviceLink]} + />, + ]} /> - </CardContent> - </Card> - ))} + <Divider /> + <CardContent> + <Incidents + readOnly={readOnly || false} + team={teamName} + refreshIncidents={refreshIncidents} + /> + {entityData?.usersHashMap && team && ( + <EscalationPolicy + team={teamName} + users={entityData?.usersHashMap} + /> + )} + <TriggerDialog + routingKey={entityData?.foundRoutingKey?.routingKey ?? teamName} + showDialog={showDialog} + handleDialog={handleDialog} + onIncidentCreated={handleRefresh} + /> + </CardContent> + </Card> + ); + })} </> ); }; diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts index 764ac94e2c..9beb4e150e 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts +++ b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import { act, fireEvent, diff --git a/plugins/stack-overflow-backend/CHANGELOG.md b/plugins/stack-overflow-backend/CHANGELOG.md index a29993ea05..d10de5de42 100644 --- a/plugins/stack-overflow-backend/CHANGELOG.md +++ b/plugins/stack-overflow-backend/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-stack-overflow-backend +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## 0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## 0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## 0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + ## 0.1.9 ### Patch Changes diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index 857ea48fba..e5cf07c515 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow-backend", - "version": "0.1.9", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/stack-overflow/CHANGELOG.md b/plugins/stack-overflow/CHANGELOG.md index a6af55488a..91ea342e8a 100644 --- a/plugins/stack-overflow/CHANGELOG.md +++ b/plugins/stack-overflow/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-stack-overflow +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-home@0.4.32-next.0 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/plugin-home@0.4.31 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## 0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-home@0.4.31-next.2 + - @backstage/plugin-search-common@1.2.1 + +## 0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home@0.4.31-next.1 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.2.1 + +## 0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home@0.4.31-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index 3d0ddc0d0c..cd0cfeabf3 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -49,7 +49,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/stack-overflow/src/api/StackOverflowClient.ts b/plugins/stack-overflow/src/api/StackOverflowClient.ts index f4ff5bf190..f598f912bd 100644 --- a/plugins/stack-overflow/src/api/StackOverflowClient.ts +++ b/plugins/stack-overflow/src/api/StackOverflowClient.ts @@ -39,9 +39,8 @@ export class StackOverflowClient implements StackOverflowApi { } /** - * List Questions in the StackOverflow instance - * - * */ + * List questions in the StackOverflow instance + */ async listQuestions(options: { requestParams: StackOverflowQuestionsRequestParams; }): Promise<StackOverflowQuestion[]> { diff --git a/plugins/stackstorm/.eslintrc.js b/plugins/stackstorm/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/stackstorm/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/stackstorm/CHANGELOG.md b/plugins/stackstorm/CHANGELOG.md new file mode 100644 index 0000000000..c6718679a3 --- /dev/null +++ b/plugins/stackstorm/CHANGELOG.md @@ -0,0 +1,15 @@ +# @backstage/plugin-stackstorm + +## 0.1.0-next.0 + +### Minor Changes + +- f3a15259e9: Add StackStorm plugin to Backstage which is interfacing with StackStorm API. It allows users to view workflow executions, packs and actions. For installation and configuration instructions please view [README.md](https://github.com/backstage/backstage/tree/master/plugins/stackstorm/README.md) + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 diff --git a/plugins/stackstorm/README.md b/plugins/stackstorm/README.md new file mode 100644 index 0000000000..0d5caeaca2 --- /dev/null +++ b/plugins/stackstorm/README.md @@ -0,0 +1,61 @@ +# StackStorm Plugin + +Welcome to the StackStorm plugin! + +A Backstage integration for the [StackStorm](https://docs.stackstorm.com/overview.html). +This plugin allows you to display a list of executions, view execution details, +browse installed packs, actions and more. + +## Getting started + +To get started, first you need a running instance of StackStorm. +One of the quickest ways is [running StackStorm with Docker](https://docs.stackstorm.com/install/docker.html). + +### Installation + +1. Install the plugin with `yarn` in the root of your Backstage directory + +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-stackstorm +``` + +2. Import and use the plugin in `packages/app/src/App.tsx` + +```tsx +import { StackstormPage } from '@backstage/plugin-stackstorm'; + +const routes = ( + <FlatRoutes> + {/* ...other routes */} + <Route path="/stackstorm" element={<StackstormPage />} /> + </FlatRoutes> +``` + +### Configuration + +1. Configure `webUrl` for links to the StackStorm Web UI in `app-config.yaml`. + +```yaml +stackstorm: + webUrl: 'https://your.stackstorm.webui.com' +``` + +2. Configure `stackstorm` proxy + This plugin uses the Backstage proxy to securely communicate with StackStorm API. + Add the following to your `app-config.yaml` to enable this configuration: + +```yaml +proxy: + '/stackstorm': + target: https://your.stackstorm.instance.com/api + headers: + St2-Api-Key: ${ST2_API_KEY} +``` + +In your production deployment of Backstage, you would also need to ensure that +you've set the `ST2_API_KEY` environment variable before starting +the backend. + +Read more about how to find or generate this key in the +[StackStorm Authentication Documentation](https://docs.stackstorm.com/authentication.html#api-keys). diff --git a/plugins/stackstorm/api-report.md b/plugins/stackstorm/api-report.md new file mode 100644 index 0000000000..3e3d79fe27 --- /dev/null +++ b/plugins/stackstorm/api-report.md @@ -0,0 +1,22 @@ +## API Report File for "@backstage/plugin-stackstorm" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// <reference types="react" /> + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public +export const StackstormPage: () => JSX.Element; + +// @public +export const stackstormPlugin: BackstagePlugin< + { + root: RouteRef<undefined>; + }, + {}, + {} +>; +``` diff --git a/plugins/stackstorm/config.d.ts b/plugins/stackstorm/config.d.ts new file mode 100644 index 0000000000..bf7b56823a --- /dev/null +++ b/plugins/stackstorm/config.d.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Config { + stackstorm?: { + /** + * StackStorm Web UI url + * Used in links to StackStorm web UI + * @visibility frontend + */ + webUrl: string; + }; +} diff --git a/plugins/stackstorm/dev/index.tsx b/plugins/stackstorm/dev/index.tsx new file mode 100644 index 0000000000..33bd89adda --- /dev/null +++ b/plugins/stackstorm/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { stackstormPlugin, StackstormPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(stackstormPlugin) + .addPage({ + element: <StackstormPage />, + title: 'Root Page', + path: '/stackstorm', + }) + .render(); diff --git a/plugins/stackstorm/package.json b/plugins/stackstorm/package.json new file mode 100644 index 0000000000..e00821cf77 --- /dev/null +++ b/plugins/stackstorm/package.json @@ -0,0 +1,66 @@ +{ + "name": "@backstage/plugin-stackstorm", + "description": "A Backstage plugin that integrates towards StackStorm", + "version": "0.1.0-next.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/stackstorm" + }, + "keywords": [ + "backstage", + "stackstorm", + "st2" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/theme": "workspace:^", + "@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" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/stackstorm/src/api/StackstormClient.test.ts b/plugins/stackstorm/src/api/StackstormClient.test.ts new file mode 100644 index 0000000000..9757bde2af --- /dev/null +++ b/plugins/stackstorm/src/api/StackstormClient.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api'; +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { StackstormClient } from './StackstormClient'; +import { Action, Execution, Pack } from './types'; + +const server = setupServer(); + +const executions: Execution[] = [ + { + id: '63dcac3e18ba00e09e7bb3b6', + action: { + name: 'post_message', + ref: 'chatops.post_message', + description: 'Post a message to stream for chatops', + pack: 'chatops', + runner_type: 'announcement', + id: '62fe101b11935b6aaff4ff92', + }, + status: 'succeeded', + start_timestamp: new Date().toISOString(), + end_timestamp: new Date().toISOString(), + result: {}, + parameters: {}, + elapsed_seconds: 2.2, + log: [], + }, + { + id: '63dcac3e18ba00e09e7bb3b6', + action: { + name: 'post_result', + ref: 'chatops.post_result', + description: 'Post an execution result to stream for chatops', + pack: 'chatops', + runner_type: 'orquesta', + id: '62fe101b11935b6aaff4ff93', + }, + status: 'succeeded', + start_timestamp: new Date().toISOString(), + end_timestamp: new Date().toISOString(), + result: {}, + parameters: {}, + elapsed_seconds: 18.5, + log: [], + }, + { + id: '63dcac3c9e0b4fe98f46becc', + action: { + name: 'run', + ref: 'shell.run', + description: 'Run shell script', + pack: 'shell', + runner_type: 'shell', + id: '63736caac3d8557c4d61883a', + }, + status: 'failed', + start_timestamp: new Date().toISOString(), + end_timestamp: new Date().toISOString(), + result: {}, + parameters: {}, + elapsed_seconds: 5.0, + log: [], + }, +]; + +const executionWithDetails: Execution = { + id: '63dcac3e18ba00e09e7bb3b6', + action: { + name: 'post_message', + ref: 'chatops.post_message', + description: 'Post a message to stream for chatops', + pack: 'chatops', + runner_type: 'announcement', + id: '62fe101b11935b6aaff4ff92', + }, + status: 'succeeded', + start_timestamp: new Date().toISOString(), + end_timestamp: new Date().toISOString(), + result: { + output: { + result: 'hello', + }, + }, + parameters: { + var: 'val', + }, + elapsed_seconds: 2.2, + log: [ + { status: 'requested', timestamp: new Date().toISOString() }, + { status: 'succeeded', timestamp: new Date().toISOString() }, + ], +}; + +const packs: Pack[] = [ + { + ref: 'chatops', + description: 'ChatOps integration pack', + version: '3.7.0', + }, + { + ref: 'core', + description: 'Basic core actions.', + version: '3.7.0', + }, +]; + +const actions: Action[] = [ + { + id: '62fe101b11935b6aaff4ff96', + name: 'announcement', + ref: 'core.announcement', + pack: 'core', + description: + 'Action that broadcasts the announcement to all stream consumers.', + runner_type: 'announcement', + }, + { + id: '62fe101b11935b6aaff4ff97', + name: 'echo', + ref: 'core.echo', + pack: 'core', + description: + 'Action that executes the Linux echo command on the localhost.', + runner_type: 'local-shell-cmd', + }, +]; + +describe('StackstormClient', () => { + setupRequestMockHandlers(server); + + const mockBaseUrl = 'http://backstage:9191/api/proxy'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + let client: StackstormClient; + + const setupHandlers = () => { + server.use( + rest.get(`${mockBaseUrl}/stackstorm/executions`, (req, res, ctx) => { + const limit = req.url.searchParams.get('limit') + ? Number(req.url.searchParams.get('limit')) + : executions.length; + const offset = req.url.searchParams.get('offset') + ? Number(req.url.searchParams.get('offset')) + : 0; + return res(ctx.json(executions.slice(offset, offset + limit))); + }), + rest.get( + `${mockBaseUrl}/stackstorm/executions/63dcac3e18ba00e09e7bb3b6`, + (_req, res, ctx) => { + return res(ctx.json(executionWithDetails)); + }, + ), + rest.get(`${mockBaseUrl}/stackstorm/packs`, (_req, res, ctx) => { + return res(ctx.json(packs)); + }), + rest.get(`${mockBaseUrl}/stackstorm/actions`, (req, res, ctx) => { + const name = req.url.searchParams.get('pack'); + return res(ctx.json(name === 'core' ? actions : [])); + }), + ); + }; + + beforeEach(() => { + setupHandlers(); + client = StackstormClient.fromConfig( + new ConfigReader({ + stackstorm: { + webUrl: 'http://stackstorm.example.com:8080', + }, + }), + { + discoveryApi: discoveryApi, + fetchApi: new MockFetchApi(), + }, + ); + }); + + it('getExecutions should return executions with emulated pagination', async () => { + const got = await client.getExecutions(2, 1); + expect(got.length).toEqual(2); + expect(got).toMatchObject(executions.slice(1, 3)); + }); + + it('getExecution should return one execution', async () => { + const got = await client.getExecution('63dcac3e18ba00e09e7bb3b6'); + expect(got).toMatchObject(executionWithDetails); + }); + + it('getPacks should return list of all packs', async () => { + const got = await client.getPacks(); + expect(got.length).toEqual(packs.length); + expect(got).toMatchObject(packs); + }); + + it('getActions should return list of actions', async () => { + const got = await client.getActions('core'); + expect(got).toMatchObject(actions); + }); + + it('getExecutionHistoryUrl should return webUrl for executions', async () => { + const got = client.getExecutionHistoryUrl('123abc'); + expect(got).toEqual('http://stackstorm.example.com:8080/?#/history/123abc'); + }); + + it('getActionUrl should return webUrl for action', async () => { + const got = client.getActionUrl('core.shell'); + expect(got).toEqual( + 'http://stackstorm.example.com:8080/?#/actions/core.shell', + ); + }); +}); diff --git a/plugins/stackstorm/src/api/StackstormClient.ts b/plugins/stackstorm/src/api/StackstormClient.ts new file mode 100644 index 0000000000..495384e8a5 --- /dev/null +++ b/plugins/stackstorm/src/api/StackstormClient.ts @@ -0,0 +1,102 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Action, Execution, Pack, StackstormApi } from './types'; +import { ConfigApi, DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; + +export class StackstormClient implements StackstormApi { + private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; + private readonly webUrl: string; + + private constructor({ + discoveryApi, + fetchApi, + webUrl, + }: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + webUrl: string; + }) { + this.discoveryApi = discoveryApi; + this.fetchApi = fetchApi; + this.webUrl = webUrl; + } + + static fromConfig( + config: ConfigApi, + dependencies: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + }, + ): StackstormClient { + return new StackstormClient({ + discoveryApi: dependencies.discoveryApi, + fetchApi: dependencies.fetchApi, + webUrl: config.getString('stackstorm.webUrl'), + }); + } + + private async get<T = any>(input: string): Promise<T> { + const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/stackstorm`; + const response = await this.fetchApi.fetch(`${apiUrl}${input}`, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) throw await ResponseError.fromResponse(response); + return (await response.json()) as T; + } + + async getExecutions(limit?: number, offset?: number): Promise<Execution[]> { + const params = { + limit: limit?.toString() || '10', + offset: offset?.toString() || '0', + include_attributes: + 'id,status,start_timestamp,action.ref,action.name,rule.ref', + parent: 'null', + }; + const path = `/executions?${new URLSearchParams(params)}`; + return this.get<Execution[]>(path); + } + + async getExecution(id: string): Promise<Execution> { + const path = `/executions/${encodeURIComponent(id)}`; + return this.get<Execution>(path); + } + + async getPacks(): Promise<Pack[]> { + return this.get<Pack[]>('/packs'); + } + + async getActions(pack: string): Promise<Action[]> { + const params = { + include_attributes: 'id,ref,name,pack,description,runner_type', + pack: pack, + }; + const path = `/actions?${new URLSearchParams(params)}`; + return this.get<Action[]>(path); + } + + getExecutionHistoryUrl(id: string): string { + return `${this.webUrl}/?#/history/${encodeURIComponent(id)}`; + } + + getActionUrl(ref: string): string { + return `${this.webUrl}/?#/actions/${encodeURIComponent(ref)}`; + } +} diff --git a/plugins/stackstorm/src/api/index.ts b/plugins/stackstorm/src/api/index.ts new file mode 100644 index 0000000000..0ce652e27b --- /dev/null +++ b/plugins/stackstorm/src/api/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { stackstormApiRef } from './types'; +export type { + Action, + Execution, + ExecutionLog, + Pack, + StackstormApi, +} from './types'; +export { StackstormClient } from './StackstormClient'; diff --git a/plugins/stackstorm/src/api/types.ts b/plugins/stackstorm/src/api/types.ts new file mode 100644 index 0000000000..6ebf3d67a6 --- /dev/null +++ b/plugins/stackstorm/src/api/types.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createApiRef } from '@backstage/core-plugin-api'; + +export const stackstormApiRef = createApiRef<StackstormApi>({ + id: 'plugin.stackstorm.service', +}); + +export type Execution = { + id: string; + action: Action; + status: string; + start_timestamp: string; + end_timestamp: string; + result: object; + parameters: object; + elapsed_seconds: number; + log: ExecutionLog[]; +}; + +export type ExecutionLog = { + status: string; + timestamp: string; +}; + +export type Action = { + id: string; + name: string; + ref: string; + pack: string; + description: string; + runner_type: string; +}; + +export type Pack = { + ref: string; + description: string; + version: string; +}; + +export interface StackstormApi { + getExecutions(limit: number, offset: number): Promise<Execution[]>; + getExecution(id: string): Promise<Execution>; + getPacks(): Promise<Pack[]>; + getActions(pack: string): Promise<Action[]>; + getExecutionHistoryUrl(id: string): string; + getActionUrl(ref: string): string; +} diff --git a/plugins/stackstorm/src/components/ActionsList/ActionsList.test.tsx b/plugins/stackstorm/src/components/ActionsList/ActionsList.test.tsx new file mode 100644 index 0000000000..2a65db427a --- /dev/null +++ b/plugins/stackstorm/src/components/ActionsList/ActionsList.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { Action, Pack, StackstormApi, stackstormApiRef } from '../../api'; +import { ActionsList, PackListItem } from './ActionsList'; + +const packs: Pack[] = [ + { + ref: 'chatops', + description: 'ChatOps integration pack', + version: '3.7.0', + }, + { + ref: 'core', + description: 'Basic core actions.', + version: '3.7.1', + }, +]; + +const actions: Action[] = [ + { + id: '62fe101b11935b6aaff4ff96', + name: 'announcement', + ref: 'core.announcement', + pack: 'core', + description: + 'Action that broadcasts the announcement to all stream consumers.', + runner_type: 'broadcast', + }, + { + id: '62fe101b11935b6aaff4ff97', + name: 'echo', + ref: 'core.echo', + pack: 'core', + description: + 'Action that executes the Linux echo command on the localhost.', + runner_type: 'local-shell-cmd', + }, +]; + +describe('ActionsList', () => { + const mockApi: jest.Mocked<StackstormApi> = { + getPacks: jest.fn().mockResolvedValue(packs), + getActions: jest.fn().mockResolvedValue(actions), + getActionUrl: jest + .fn() + .mockResolvedValue( + 'http://stackstorm.example.com:8080/?#/actions/core.action', + ), + } as any; + + it('should render all packs', async () => { + const { getByText } = await renderInTestApp( + <TestApiProvider apis={[[stackstormApiRef, mockApi]]}> + <ActionsList /> + </TestApiProvider>, + ); + + packs.forEach(p => { + expect(getByText(p.ref)).toBeInTheDocument(); + expect(getByText(p.description)).toBeInTheDocument(); + }); + }); + + it('should render all pack actions', async () => { + const { getByText } = await renderInTestApp( + <TestApiProvider apis={[[stackstormApiRef, mockApi]]}> + <PackListItem pack={packs[1]} opened onClick={() => {}} /> + </TestApiProvider>, + ); + + actions.forEach(a => { + expect(getByText(a.name)).toBeInTheDocument(); + expect(getByText(a.runner_type)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/stackstorm/src/components/ActionsList/ActionsList.tsx b/plugins/stackstorm/src/components/ActionsList/ActionsList.tsx new file mode 100644 index 0000000000..1080f67373 --- /dev/null +++ b/plugins/stackstorm/src/components/ActionsList/ActionsList.tsx @@ -0,0 +1,159 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { Link, Progress, ResponseErrorPanel } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { + List, + ListItemText, + Collapse, + ListItem, + ListItemSecondaryAction, + ListItemIcon, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +import ExpandLess from '@material-ui/icons/ExpandLess'; +import { Action, Pack, stackstormApiRef } from '../../api'; + +const useStyles = makeStyles(theme => ({ + root: { + width: '100%', + backgroundColor: theme.palette.background.paper, + }, + actions: { + borderBottom: `2px solid ${theme.palette.divider}`, + }, + nested: { + paddingLeft: theme.spacing(8), + paddingRight: theme.spacing(4), + }, + icon: { + minWidth: '34px', + }, +})); + +type ActionItemsProps = { + pack: Pack; +}; + +export const ActionItems = ({ pack }: ActionItemsProps) => { + const classes = useStyles(); + const st2 = useApi(stackstormApiRef); + + const { value, loading, error } = useAsync(async (): Promise<Action[]> => { + const data = await st2.getActions(pack.ref); + return data; + }, []); + + if (loading) { + return <Progress />; + } else if (error) { + return <ResponseErrorPanel error={error} />; + } + + return ( + <List component="div" disablePadding className={classes.actions}> + {(value || []).map(a => { + return ( + <ListItem + key={a.ref} + component={Link} + to={st2.getActionUrl(a.ref)} + className={classes.nested} + underline="none" + color="inherit" + button + > + <ListItemText primary={a.name} secondary={a.description} /> + <ListItemSecondaryAction>{a.runner_type}</ListItemSecondaryAction> + </ListItem> + ); + })} + </List> + ); +}; + +type PackListItemProps = { + pack: Pack; + opened: boolean; + onClick: (ref: string) => any; +}; + +export const PackListItem = ({ pack, opened, onClick }: PackListItemProps) => { + const classes = useStyles(); + + return ( + <> + <ListItem button onClick={() => onClick(pack.ref)}> + <ListItemIcon className={classes.icon}> + {opened ? <ExpandLess /> : <ExpandMore />} + </ListItemIcon> + <ListItemText primary={pack.ref} secondary={pack.description} /> + <ListItemSecondaryAction> + version: {pack.version} + </ListItemSecondaryAction> + </ListItem> + <Collapse in={opened} timeout="auto" unmountOnExit> + <ActionItems pack={pack} /> + </Collapse> + </> + ); +}; + +export const ActionsList = () => { + const st2 = useApi(stackstormApiRef); + + const classes = useStyles(); + const [expanded, setExpanded] = useState<string[]>([]); + + const onClick = (ref: string) => { + setExpanded(refs => + refs.includes(ref) ? refs.filter(r => r !== ref) : refs.concat(ref), + ); + }; + + const { value, loading, error } = useAsync(async (): Promise<Pack[]> => { + const data = await st2.getPacks(); + return data; + }, []); + + if (loading) { + return <Progress />; + } else if (error) { + return <ResponseErrorPanel error={error} />; + } + + return ( + <List + component="nav" + aria-labelledby="nested-list-subheader" + className={classes.root} + > + {(value || []).map(p => { + return ( + <PackListItem + key={p.ref} + pack={p} + opened={expanded.includes(p.ref)} + onClick={onClick} + /> + ); + })} + </List> + ); +}; diff --git a/plugins/stackstorm/src/components/ActionsList/index.ts b/plugins/stackstorm/src/components/ActionsList/index.ts new file mode 100644 index 0000000000..1606d1be3d --- /dev/null +++ b/plugins/stackstorm/src/components/ActionsList/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ActionsList } from './ActionsList'; diff --git a/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.test.tsx b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.test.tsx new file mode 100644 index 0000000000..53930d339f --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { Execution, StackstormApi, stackstormApiRef } from '../../api'; +import { ExecutionPanel } from './ExecutionPanel'; + +const execution: Execution = { + id: '63dcac3e18ba00e09e7bb3b6', + action: { + name: 'post_message', + ref: 'chatops.post_message', + description: 'Post a message to stream for chatops', + pack: 'chatops', + runner_type: 'announcement', + id: '62fe101b11935b6aaff4ff92', + }, + status: 'succeeded', + start_timestamp: new Date().toISOString(), + end_timestamp: new Date().toISOString(), + result: { + output: { + result: 'hello world!', + }, + }, + parameters: { + var: 'my funky partameter', + }, + elapsed_seconds: 2.2, + log: [ + { status: 'requested', timestamp: new Date().toISOString() }, + { status: 'succeeded', timestamp: new Date().toISOString() }, + ], +}; + +describe('ExecutionPanel', () => { + const mockApi: jest.Mocked<StackstormApi> = { + getExecution: jest.fn().mockResolvedValue(execution), + getExecutionHistoryUrl: jest + .fn() + .mockResolvedValue( + 'http://stackstorm.example.com:8080/?#/history/63dcac3e18ba00e09e7bb3b6', + ), + } as any; + + it('should render execution details', async () => { + const { getByText } = await renderInTestApp( + <TestApiProvider apis={[[stackstormApiRef, mockApi]]}> + <ExecutionPanel id="63dcac3e18ba00e09e7bb3b6" /> + </TestApiProvider>, + ); + + expect(getByText(execution.id)).toBeInTheDocument(); + expect(getByText(execution.status)).toBeInTheDocument(); + expect(getByText('"hello world!"')).toBeInTheDocument(); + expect(getByText('"my funky partameter"')).toBeInTheDocument(); + }); +}); diff --git a/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.tsx b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.tsx new file mode 100644 index 0000000000..8a55c31246 --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.tsx @@ -0,0 +1,170 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + CodeSnippet, + Progress, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { Execution, stackstormApiRef } from '../../api'; +import useAsync from 'react-use/lib/useAsync'; +import { + Button, + Card, + CardActions, + CardContent, + makeStyles, + Table, + TableBody, + TableCell, + TableRow, + Typography, + withStyles, +} from '@material-ui/core'; +import { Status } from './Status'; + +const useStyles = makeStyles(theme => ({ + table: { + maxWidth: '50%', + flex: 'i', + }, + title: { + paddingTop: theme.spacing(2), + fontSize: 14, + fontWeight: 'bold', + textTransform: 'uppercase', + }, + card: { + borderBottom: `2px solid ${theme.palette.divider}`, + }, +})); + +const THead = withStyles(() => ({ + root: { + paddingLeft: 0, + }, +}))(TableCell); + +const TRow = withStyles(theme => ({ + root: { + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.background.paper, + }, + }, +}))(TableRow); + +const ExecutionCard = ({ e }: { e: Execution }) => { + const st2 = useApi(stackstormApiRef); + const classes = useStyles(); + + return ( + <Card className={classes.card}> + <CardContent> + <Table className={classes.table} size="small"> + <TableBody> + <TRow> + <THead component="th" scope="row"> + Name + </THead> + <TableCell>{e.action.ref}</TableCell> + </TRow> + <TRow> + <THead component="th" scope="row"> + Status + </THead> + <TableCell> + <Status status={e.status} /> + </TableCell> + </TRow> + <TRow> + <THead component="th" scope="row"> + Execution ID + </THead> + <TableCell>{e.id}</TableCell> + </TRow> + <TRow> + <THead component="th" scope="row"> + Started + </THead> + <TableCell>{new Date(e.start_timestamp).toUTCString()}</TableCell> + </TRow> + <TRow> + <THead component="th" scope="row"> + Finished + </THead> + <TableCell>{new Date(e.end_timestamp).toUTCString()}</TableCell> + </TRow> + <TRow> + <THead component="th" scope="row"> + Execution Time + </THead> + <TableCell>{Math.round(e.elapsed_seconds)} s</TableCell> + </TRow> + <TRow> + <THead component="th" scope="row"> + Runner + </THead> + <TableCell>{e.action.runner_type}</TableCell> + </TRow> + </TableBody> + </Table> + <Typography className={classes.title} gutterBottom> + Action Output + </Typography> + <CodeSnippet + text={JSON.stringify(e.result, null, 2)} + language="json" + customStyle={{ width: 800 }} + /> + <Typography className={classes.title} gutterBottom> + Action Input + </Typography> + <CodeSnippet + text={JSON.stringify(e.parameters, null, 2)} + language="json" + customStyle={{ width: 800 }} + /> + </CardContent> + <CardActions> + <Button + size="small" + href={`${st2.getExecutionHistoryUrl(e.id)}`} + target="_blank" + > + View in ST2 + </Button> + </CardActions> + </Card> + ); +}; + +export const ExecutionPanel = ({ id }: { id: string }) => { + const st2 = useApi(stackstormApiRef); + + const { value, loading, error } = useAsync(async (): Promise<Execution> => { + const data = await st2.getExecution(id); + return data; + }, []); + + if (loading) { + return <Progress />; + } else if (error) { + return <ResponseErrorPanel error={error} />; + } + + return <ExecutionCard e={value!} />; +}; diff --git a/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.test.tsx b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.test.tsx new file mode 100644 index 0000000000..6727f3cc56 --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.test.tsx @@ -0,0 +1,101 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { Execution, StackstormApi, stackstormApiRef } from '../../api'; +import { ExecutionsTable } from './ExecutionsTable'; + +const executions: Execution[] = [ + { + id: '63dcac3e18ba00e09e7bb3b6', + action: { + name: 'post_message', + ref: 'chatops.post_message', + description: 'Post a message to stream for chatops', + pack: 'chatops', + runner_type: 'announcement', + id: '62fe101b11935b6aaff4ff92', + }, + status: 'processing', + start_timestamp: new Date().toISOString(), + end_timestamp: new Date().toISOString(), + result: {}, + parameters: {}, + elapsed_seconds: 2.2, + log: [], + }, + { + id: '62fe101b11935b6aaff4ff93', + action: { + name: 'post_result', + ref: 'chatops.post_result', + description: 'Post an execution result to stream for chatops', + pack: 'chatops', + runner_type: 'orquesta', + id: '62fe101b11935b6aaff4ff93', + }, + status: 'succeeded', + start_timestamp: new Date().toISOString(), + end_timestamp: new Date().toISOString(), + result: {}, + parameters: {}, + elapsed_seconds: 18.5, + log: [], + }, + { + id: '63dcac3c9e0b4fe98f46becc', + action: { + name: 'run', + ref: 'shell.run', + description: 'Run shell script', + pack: 'shell', + runner_type: 'shell', + id: '63736caac3d8557c4d61883a', + }, + status: 'failed', + start_timestamp: new Date().toISOString(), + end_timestamp: new Date().toISOString(), + result: {}, + parameters: {}, + elapsed_seconds: 5.0, + log: [], + }, +]; + +describe('ExecutionsTable', () => { + const mockApi: jest.Mocked<StackstormApi> = { + getExecutions: jest.fn().mockResolvedValue(executions), + getExecutionHistoryUrl: jest + .fn() + .mockResolvedValue( + 'http://stackstorm.example.com:8080/?#/history/123abc', + ), + } as any; + + it('should render all executions', async () => { + const { getByText } = await renderInTestApp( + <TestApiProvider apis={[[stackstormApiRef, mockApi]]}> + <ExecutionsTable /> + </TestApiProvider>, + ); + + executions.forEach(e => { + expect(getByText(e.id)).toBeInTheDocument(); + expect(getByText(e.action.ref)).toBeInTheDocument(); + expect(getByText(e.status)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.tsx b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.tsx new file mode 100644 index 0000000000..f034cb64e8 --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; +import { + Link, + ResponseErrorPanel, + Table, + TableColumn, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { Execution, stackstormApiRef } from '../../api'; +import { Status } from './Status'; +import { ExecutionPanel } from './ExecutionPanel'; +import useAsync from 'react-use/lib/useAsync'; + +type DenseTableProps = { + executions: Execution[]; + loading: boolean; + page: number; + pageSize: number; + onPageChange: (page: number) => void; + onRowsPerPageChange: (rows: number) => void; +}; + +export const DenseTable = ({ + executions, + loading, + page, + pageSize, + onPageChange, + onRowsPerPageChange, +}: DenseTableProps) => { + const st2 = useApi(stackstormApiRef); + + const columns: TableColumn<Execution>[] = [ + { + title: 'Status', + field: 'status', + render: e => <Status status={e.status} />, + }, + { + title: 'Time', + field: 'start_timestamp', + render: e => `${new Date(e.start_timestamp).toUTCString()}`, + }, + { title: 'Name', field: 'action.ref' }, + { + title: 'Execution ID', + field: 'id', + render: e => <Link to={st2.getExecutionHistoryUrl(e.id)}>{e.id}</Link>, + }, + ]; + + const count = + pageSize > executions.length + ? (page + 1) * pageSize + executions.length - pageSize + : (page + 1) * pageSize + 1; + + return ( + <Table + title="Executions" + columns={columns} + data={executions} + page={page} + totalCount={count} + isLoading={loading} + options={{ + paging: true, + search: false, + pageSize: pageSize, + padding: 'dense', + showFirstLastPageButtons: false, + }} + onPageChange={onPageChange} + onRowsPerPageChange={onRowsPerPageChange} + detailPanel={rowData => { + return <ExecutionPanel id={rowData.rowData.id} />; + }} + /> + ); +}; + +export const ExecutionsTable = () => { + const st2 = useApi(stackstormApiRef); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + + const { value, loading, error } = useAsync(async (): Promise<Execution[]> => { + const data = await st2.getExecutions(rowsPerPage, page * rowsPerPage); + return data; + }, [page, rowsPerPage, st2]); + + if (error) { + return <ResponseErrorPanel error={error} />; + } + + return ( + <DenseTable + page={page} + pageSize={rowsPerPage} + loading={loading} + executions={value || []} + onRowsPerPageChange={setRowsPerPage} + onPageChange={setPage} + /> + ); +}; diff --git a/plugins/stackstorm/src/components/ExecutionsTable/Status.tsx b/plugins/stackstorm/src/components/ExecutionsTable/Status.tsx new file mode 100644 index 0000000000..99a94df2c8 --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/Status.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + StatusOK, + StatusError, + StatusRunning, + StatusWarning, +} from '@backstage/core-components'; +import React from 'react'; + +export const Status = ({ status }: { status: string | undefined }) => { + if (status === undefined) return null; + const st = status.toLocaleLowerCase('en-US'); + switch (status) { + case 'succeeded': + case 'enabled': + case 'complete': + return ( + <> + <StatusOK /> {st} + </> + ); + case 'scheduled': + case 'running': + return ( + <> + <StatusRunning /> {st} + </> + ); + case 'failed': + case 'error': + return ( + <> + <StatusError /> {st} + </> + ); + default: + return ( + <> + <StatusWarning /> {st} + </> + ); + } +}; diff --git a/plugins/stackstorm/src/components/ExecutionsTable/index.ts b/plugins/stackstorm/src/components/ExecutionsTable/index.ts new file mode 100644 index 0000000000..402d8e557d --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ExecutionsTable } from './ExecutionsTable'; diff --git a/plugins/stackstorm/src/components/PacksTable/PacksTable.test.tsx b/plugins/stackstorm/src/components/PacksTable/PacksTable.test.tsx new file mode 100644 index 0000000000..9de17e2f14 --- /dev/null +++ b/plugins/stackstorm/src/components/PacksTable/PacksTable.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { Pack, StackstormApi, stackstormApiRef } from '../../api'; +import { PacksTable } from './PacksTable'; + +const packs: Pack[] = [ + { + ref: 'chatops', + description: 'ChatOps integration pack', + version: '3.7.0', + }, + { + ref: 'core', + description: 'Basic core actions.', + version: '3.7.1', + }, +]; + +describe('PacksTable', () => { + const mockApi: jest.Mocked<StackstormApi> = { + getPacks: jest.fn().mockResolvedValue(packs), + } as any; + + it('should render all packs', async () => { + const { getByText } = await renderInTestApp( + <TestApiProvider apis={[[stackstormApiRef, mockApi]]}> + <PacksTable /> + </TestApiProvider>, + ); + + packs.forEach(p => { + expect(getByText(p.ref)).toBeInTheDocument(); + expect(getByText(p.description)).toBeInTheDocument(); + expect(getByText(p.version)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/stackstorm/src/components/PacksTable/PacksTable.tsx b/plugins/stackstorm/src/components/PacksTable/PacksTable.tsx new file mode 100644 index 0000000000..2167200e8e --- /dev/null +++ b/plugins/stackstorm/src/components/PacksTable/PacksTable.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + Progress, + ResponseErrorPanel, + Table, + TableColumn, +} from '@backstage/core-components'; +import useAsync from 'react-use/lib/useAsync'; +import { useApi } from '@backstage/core-plugin-api'; +import { Pack, stackstormApiRef } from '../../api'; + +type DenseTableProps = { + packs: Pack[]; +}; + +export const DenseTable = ({ packs }: DenseTableProps) => { + const columns: TableColumn[] = [ + { title: 'Name', field: 'ref' }, + { title: 'Description', field: 'description' }, + { title: 'Version', field: 'version' }, + ]; + + return ( + <Table + title="Packs" + options={{ search: true, paging: false }} + columns={columns} + data={packs} + /> + ); +}; + +export const PacksTable = () => { + const st2 = useApi(stackstormApiRef); + + const { value, loading, error } = useAsync(async (): Promise<Pack[]> => { + const data = await st2.getPacks(); + return data; + }, []); + + if (loading) { + return <Progress />; + } else if (error) { + return <ResponseErrorPanel error={error} />; + } + + return <DenseTable packs={value || []} />; +}; diff --git a/plugins/stackstorm/src/components/PacksTable/index.ts b/plugins/stackstorm/src/components/PacksTable/index.ts new file mode 100644 index 0000000000..378e32a5d5 --- /dev/null +++ b/plugins/stackstorm/src/components/PacksTable/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { PacksTable } from './PacksTable'; diff --git a/plugins/stackstorm/src/components/StackstormHome/StackstormHome.test.tsx b/plugins/stackstorm/src/components/StackstormHome/StackstormHome.test.tsx new file mode 100644 index 0000000000..897cec61c1 --- /dev/null +++ b/plugins/stackstorm/src/components/StackstormHome/StackstormHome.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { StackstormHome } from './StackstormHome'; +import { screen } from '@testing-library/react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { StackstormApi, stackstormApiRef } from '../../api'; + +describe('StackstormHome', () => { + const mockApi: jest.Mocked<StackstormApi> = { + getExecutions: jest.fn().mockResolvedValue([]), + getExecution: jest.fn().mockResolvedValue({}), + getPacks: jest.fn().mockResolvedValue([]), + getActions: jest.fn().mockResolvedValue([]), + } as any; + + it('should render', async () => { + await renderInTestApp( + <TestApiProvider apis={[[stackstormApiRef, mockApi]]}> + <StackstormHome /> + </TestApiProvider>, + ); + expect(screen.getByText('Welcome to StackStorm!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/stackstorm/src/components/StackstormHome/StackstormHome.tsx b/plugins/stackstorm/src/components/StackstormHome/StackstormHome.tsx new file mode 100644 index 0000000000..d96d45e82d --- /dev/null +++ b/plugins/stackstorm/src/components/StackstormHome/StackstormHome.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { IconButton } from '@material-ui/core'; +import { + Header, + Page, + HeaderLabel, + TabbedLayout, + Content, +} from '@backstage/core-components'; +import LibraryBooks from '@material-ui/icons/LibraryBooks'; +import { ExecutionsTable } from '../ExecutionsTable'; +import { PacksTable } from '../PacksTable/PacksTable'; +import { ActionsList } from '../ActionsList'; + +export const StackstormHome = () => ( + <Page themeId="tool"> + <Header title="Welcome to StackStorm!" subtitle="Event-driven automation"> + <IconButton aria-label="Docs" href="https://docs.stackstorm.com/"> + <LibraryBooks htmlColor="white" /> + </IconButton> + <HeaderLabel label="Lifecycle" value="Alpha" /> + </Header> + <TabbedLayout> + <TabbedLayout.Route path="/history" title="Executions"> + <Content noPadding> + <ExecutionsTable /> + </Content> + </TabbedLayout.Route> + <TabbedLayout.Route path="/packs" title="Packs"> + <Content noPadding> + <PacksTable /> + </Content> + </TabbedLayout.Route> + <TabbedLayout.Route path="/actions" title="Actions"> + <Content noPadding> + <ActionsList /> + </Content> + </TabbedLayout.Route> + </TabbedLayout> + </Page> +); diff --git a/plugins/stackstorm/src/components/StackstormHome/index.ts b/plugins/stackstorm/src/components/StackstormHome/index.ts new file mode 100644 index 0000000000..2063d21f34 --- /dev/null +++ b/plugins/stackstorm/src/components/StackstormHome/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { StackstormHome } from './StackstormHome'; diff --git a/plugins/stackstorm/src/index.ts b/plugins/stackstorm/src/index.ts new file mode 100644 index 0000000000..b64ae207e4 --- /dev/null +++ b/plugins/stackstorm/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A Backstage plugin that integrates towards StackStorm + * + * @packageDocumentation + */ + +export { stackstormPlugin, StackstormPage } from './plugin'; diff --git a/plugins/stackstorm/src/plugin.test.ts b/plugins/stackstorm/src/plugin.test.ts new file mode 100644 index 0000000000..9ef2f438c1 --- /dev/null +++ b/plugins/stackstorm/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { stackstormPlugin } from './plugin'; + +describe('stackstorm', () => { + it('should export plugin', () => { + expect(stackstormPlugin).toBeDefined(); + }); +}); diff --git a/plugins/stackstorm/src/plugin.ts b/plugins/stackstorm/src/plugin.ts new file mode 100644 index 0000000000..be4b9c065c --- /dev/null +++ b/plugins/stackstorm/src/plugin.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + configApiRef, + createApiFactory, + createPlugin, + createRoutableExtension, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; +import { stackstormApiRef, StackstormClient } from './api'; +import { rootRouteRef } from './routes'; + +/** + * The Backstage plugin that holds stackstorm specific components + * + * @public + */ +export const stackstormPlugin = createPlugin({ + id: 'stackstorm', + apis: [ + createApiFactory({ + api: stackstormApiRef, + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ configApi, discoveryApi, fetchApi }) => + StackstormClient.fromConfig(configApi, { + discoveryApi, + fetchApi, + }), + }), + ], + routes: { + root: rootRouteRef, + }, +}); + +/** + * A component to display a stackstorm home page + * + * @public + */ +export const StackstormPage = stackstormPlugin.provide( + createRoutableExtension({ + name: 'StackstormPage', + component: () => + import('./components/StackstormHome').then(m => m.StackstormHome), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/stackstorm/src/routes.ts b/plugins/stackstorm/src/routes.ts new file mode 100644 index 0000000000..90cf7760bc --- /dev/null +++ b/plugins/stackstorm/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'stackstorm', +}); diff --git a/plugins/stackstorm/src/setupTests.ts b/plugins/stackstorm/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/stackstorm/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index 91b5a6ea8e..d97c864580 100644 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-tech-insights-backend-module-jsonfc +## 0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/plugin-tech-insights-node@0.4.1-next.0 + +## 0.1.26 + +### Patch Changes + +- d6b912f963: Surface the cause of the json rules engine +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/plugin-tech-insights-node@0.4.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + +## 0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.1.26-next.0 + +### Patch Changes + +- d6b912f963: Surface the cause of the json rules engine +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-tech-insights-node@0.3.10-next.0 + ## 0.1.24 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index b2ec55e821..062829135e 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.24", + "version": "0.1.27-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index 97031a15d7..262e5a6d0a 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,101 @@ # @backstage/plugin-tech-insights-backend +## 0.5.9-next.0 + +### Patch Changes + +- f244b58916: Add DB index to improve latency of latest fact query +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/plugin-tech-insights-node@0.4.1-next.0 + +## 0.5.8 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/plugin-tech-insights-node@0.4.0 + - @backstage/catalog-model@1.2.0 + - @backstage/backend-tasks@0.4.3 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + +## 0.5.8-next.1 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.1 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.5.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + - @backstage/plugin-tech-insights-node@0.3.10-next.0 + ## 0.5.6 ### Patch Changes diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index a757f0d67c..4b6e5b34dc 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -12,7 +12,7 @@ import { FactCheckerFactory } from '@backstage/plugin-tech-insights-node'; import { FactLifecycle } from '@backstage/plugin-tech-insights-node'; import { FactRetriever } from '@backstage/plugin-tech-insights-node'; import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-node'; -import { FactSchema } from '@backstage/plugin-tech-insights-node'; +import { FactSchema } from '@backstage/plugin-tech-insights-common'; import { HumanDuration } from '@backstage/types'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; diff --git a/plugins/tech-insights-backend/migrations/20230213170839_latest-facts-index.js b/plugins/tech-insights-backend/migrations/20230213170839_latest-facts-index.js new file mode 100644 index 0000000000..94a589f59d --- /dev/null +++ b/plugins/tech-insights-backend/migrations/20230213170839_latest-facts-index.js @@ -0,0 +1,36 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const indexName = 'fact_id_entity_timestamp_idx'; +/** + * @param { import("knex").Knex } knex + * @returns { Promise<void> } + */ +exports.up = async knex => { + await knex.schema.alterTable('facts', table => { + table.index(['id', 'entity', 'timestamp'], indexName); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise<void> } + */ +exports.down = async knex => { + await knex.schema.alterTable('facts', table => { + table.dropIndex(indexName); + }); +}; diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index 06fab9fde1..98152f39ac 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.5.6", + "version": "0.5.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts index 62adbad8e3..8796f1511a 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts @@ -17,8 +17,8 @@ import { FactRetriever, FactRetrieverRegistration, - FactSchema, } from '@backstage/plugin-tech-insights-node'; +import { FactSchema } from '@backstage/plugin-tech-insights-common'; import { ConflictError, NotFoundError } from '@backstage/errors'; /** diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts index 7c2ba52717..c8f098a895 100644 --- a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts @@ -16,12 +16,12 @@ import { Knex } from 'knex'; import { FactLifecycle, - FactSchema, FactSchemaDefinition, FlatTechInsightFact, TechInsightFact, TechInsightsStore, } from '@backstage/plugin-tech-insights-node'; +import { FactSchema } from '@backstage/plugin-tech-insights-common'; import { rsort } from 'semver'; import { groupBy, omit } from 'lodash'; import { DateTime } from 'luxon'; diff --git a/plugins/tech-insights-common/CHANGELOG.md b/plugins/tech-insights-common/CHANGELOG.md index 555a77689f..bf0f8f62ba 100644 --- a/plugins/tech-insights-common/CHANGELOG.md +++ b/plugins/tech-insights-common/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-tech-insights-common +## 0.2.10 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/types@1.0.2 + +## 0.2.10-next.0 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/types@1.0.2 + ## 0.2.9 ### Patch Changes diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md index 0236c0619a..51ef13a013 100644 --- a/plugins/tech-insights-common/api-report.md +++ b/plugins/tech-insights-common/api-report.md @@ -47,5 +47,22 @@ export type FactResponse = { }; }; +// @public +export type FactSchema = { + [name: string]: { + type: + | 'integer' + | 'float' + | 'string' + | 'boolean' + | 'datetime' + | 'set' + | 'object'; + description: string; + since?: string; + metadata?: Record<string, any>; + }; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index f238785e54..bb6b6e9a57 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.9", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-common/src/index.ts b/plugins/tech-insights-common/src/index.ts index 66af5c2f93..a74c1f16e9 100644 --- a/plugins/tech-insights-common/src/index.ts +++ b/plugins/tech-insights-common/src/index.ts @@ -134,3 +134,60 @@ export type BulkCheckResponse = Array<{ entity: string; results: CheckResult[]; }>; + +/** + * A record type to specify individual fact shapes + * + * Used as part of a schema to validate, identify and generically construct usage implementations + * of individual fact values in the system. + * + * @public + */ +export type FactSchema = { + /** + * Name of the fact + */ + [name: string]: { + /** + * Type of the individual fact value + * + * Numbers are split into integers and floating point values. + * `set` indicates a collection of values, `object` indicates JSON serializable value + */ + type: + | 'integer' + | 'float' + | 'string' + | 'boolean' + | 'datetime' + | 'set' + | 'object'; + + /** + * A description of this individual fact value + */ + description: string; + + /** + * Optional semver string to indicate when this specific fact definition was added to the schema + */ + since?: string; + + /** + * Metadata related to an individual fact. + * Can contain links, additional description texts and other actionable data. + * + * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined + * + * examples: + * ``` + * \{ + * link: 'https://sonarqube.mycompany.com/fix-these-issues', + * suggestion: 'To affect this value, you can do x, y, z', + * minValue: 0 + * \} + * ``` + */ + metadata?: Record<string, any>; + }; +}; diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md index d0b78e9fac..84694fc518 100644 --- a/plugins/tech-insights-node/CHANGELOG.md +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -1,5 +1,85 @@ # @backstage/plugin-tech-insights-node +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10 + +## 0.4.0 + +### Minor Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.4.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + +## 0.4.0-next.1 + +### Minor Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## 0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + ## 0.3.8 ### Patch Changes diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index 47e76b07ce..dbb8a8b674 100644 --- a/plugins/tech-insights-node/api-report.md +++ b/plugins/tech-insights-node/api-report.md @@ -8,6 +8,7 @@ import { Config } from '@backstage/config'; import { DateTime } from 'luxon'; import { Duration } from 'luxon'; import { DurationLike } from 'luxon'; +import { FactSchema } from '@backstage/plugin-tech-insights-common'; import { HumanDuration } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Logger } from 'winston'; @@ -78,23 +79,6 @@ export type FactRetrieverRegistration = { initialDelay?: Duration | HumanDuration; }; -// @public -export type FactSchema = { - [name: string]: { - type: - | 'integer' - | 'float' - | 'string' - | 'boolean' - | 'datetime' - | 'set' - | 'object'; - description: string; - since?: string; - metadata?: Record<string, any>; - }; -}; - // @public export type FactSchemaDefinition = Omit<FactRetriever, 'handler'>; diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index a0c9786504..e6a06bd615 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-node", - "version": "0.3.8", + "version": "0.4.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index a4a0834970..b35ad756ed 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -20,6 +20,7 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; +import { FactSchema } from '@backstage/plugin-tech-insights-common'; import { Logger } from 'winston'; /** @@ -79,63 +80,6 @@ export type FlatTechInsightFact = TechInsightFact & { id: string; }; -/** - * A record type to specify individual fact shapes - * - * Used as part of a schema to validate, identify and generically construct usage implementations - * of individual fact values in the system. - * - * @public - */ -export type FactSchema = { - /** - * Name of the fact - */ - [name: string]: { - /** - * Type of the individual fact value - * - * Numbers are split into integers and floating point values. - * `set` indicates a collection of values, `object` indicates JSON serializable value - */ - type: - | 'integer' - | 'float' - | 'string' - | 'boolean' - | 'datetime' - | 'set' - | 'object'; - - /** - * A description of this individual fact value - */ - description: string; - - /** - * Optional semver string to indicate when this specific fact definition was added to the schema - */ - since?: string; - - /** - * Metadata related to an individual fact. - * Can contain links, additional description texts and other actionable data. - * - * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined - * - * examples: - * ``` - * \{ - * link: 'https://sonarqube.mycompany.com/fix-these-issues', - * suggestion: 'To affect this value, you can do x, y, z', - * minValue: 0 - * \} - * ``` - */ - metadata?: Record<string, any>; - }; -}; - /** * @public * @@ -218,7 +162,7 @@ export interface FactRetriever { * \{ timeToLive: 1209600000 \} * \{ timeToLive: \{ weeks: 4 \} \} * - **/ + */ export type TTL = { timeToLive: DurationLike }; /** @@ -228,7 +172,7 @@ export type TTL = { timeToLive: DurationLike }; * @example * \{ maxItems: 10 \} * - **/ + */ export type MaxItems = { maxItems: number }; /** diff --git a/plugins/tech-insights-node/src/persistence.ts b/plugins/tech-insights-node/src/persistence.ts index 31fda403a8..73abb6c5af 100644 --- a/plugins/tech-insights-node/src/persistence.ts +++ b/plugins/tech-insights-node/src/persistence.ts @@ -14,13 +14,13 @@ * limitations under the License. */ import { - FactSchema, TechInsightFact, FlatTechInsightFact, FactSchemaDefinition, FactLifecycle, } from './facts'; import { DateTime } from 'luxon'; +import { FactSchema } from '@backstage/plugin-tech-insights-common'; /** * TechInsights Database diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index f26f9f910e..1703832055 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,93 @@ # @backstage/plugin-tech-insights +## 0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10 + +## 0.3.7 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/plugin-tech-insights-common@0.2.10 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + +## 0.3.7-next.1 + +### Patch Changes + +- 4024b37449: TechInsightsApi interface now has getFactSchemas() method. + TechInsightsClient now implements method getFactSchemas(). + + **BREAKING** FactSchema type moved from @backstage/plugin-tech-insights-node into @backstage/plugin-tech-insights-common + + These changes are **required** if you were importing this type directly. + + ```diff + - import { FactSchema } from '@backstage/plugin-tech-insights-node'; + + import { FactSchema } from '@backstage/plugin-tech-insights-common'; + ``` + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## 0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.3.6 ### Patch Changes diff --git a/plugins/tech-insights/api-report.md b/plugins/tech-insights/api-report.md index 2d25d6d39d..d7ae337196 100644 --- a/plugins/tech-insights/api-report.md +++ b/plugins/tech-insights/api-report.md @@ -11,6 +11,7 @@ import { BulkCheckResponse } from '@backstage/plugin-tech-insights-common'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { FactSchema } from '@backstage/plugin-tech-insights-common'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonValue } from '@backstage/types'; import { default as React_2 } from 'react'; @@ -74,6 +75,8 @@ export interface TechInsightsApi { // (undocumented) getFacts(entity: CompoundEntityRef, facts: string[]): Promise<InsightFacts>; // (undocumented) + getFactSchemas(): Promise<FactSchema[]>; + // (undocumented) runBulkChecks( entities: CompoundEntityRef[], checks?: Check[], @@ -102,6 +105,8 @@ export class TechInsightsClient implements TechInsightsApi { // (undocumented) getFacts(entity: CompoundEntityRef, facts: string[]): Promise<InsightFacts>; // (undocumented) + getFactSchemas(): Promise<FactSchema[]>; + // (undocumented) runBulkChecks( entities: CompoundEntityRef[], checks?: Check[], diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index 221445f1be..25b72e0de5 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights", - "version": "0.3.6", + "version": "0.3.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -56,7 +56,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/tech-insights/src/api/TechInsightsApi.ts b/plugins/tech-insights/src/api/TechInsightsApi.ts index 166f1d2782..bb915e06dd 100644 --- a/plugins/tech-insights/src/api/TechInsightsApi.ts +++ b/plugins/tech-insights/src/api/TechInsightsApi.ts @@ -18,6 +18,7 @@ import { createApiRef } from '@backstage/core-plugin-api'; import { CheckResult, BulkCheckResponse, + FactSchema, } from '@backstage/plugin-tech-insights-common'; import { Check, InsightFacts } from './types'; import { CheckResultRenderer } from '../components/CheckResultRenderer'; @@ -49,4 +50,5 @@ export interface TechInsightsApi { checks?: Check[], ): Promise<BulkCheckResponse>; getFacts(entity: CompoundEntityRef, facts: string[]): Promise<InsightFacts>; + getFactSchemas(): Promise<FactSchema[]>; } diff --git a/plugins/tech-insights/src/api/TechInsightsClient.ts b/plugins/tech-insights/src/api/TechInsightsClient.ts index 6e85769b34..a1a2e7ff77 100644 --- a/plugins/tech-insights/src/api/TechInsightsClient.ts +++ b/plugins/tech-insights/src/api/TechInsightsClient.ts @@ -18,6 +18,7 @@ import { TechInsightsApi } from './TechInsightsApi'; import { BulkCheckResponse, CheckResult, + FactSchema, } from '@backstage/plugin-tech-insights-common'; import { Check, InsightFacts } from './types'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; @@ -69,6 +70,10 @@ export class TechInsightsClient implements TechInsightsApi { return this.api('/checks'); } + async getFactSchemas(): Promise<FactSchema[]> { + return this.api('/fact-schemas'); + } + async runChecks( entityParams: CompoundEntityRef, checks?: string[], diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 085aa42aa9..0ac00e3cfe 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,75 @@ # @backstage/plugin-tech-radar +## 0.6.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/theme@0.2.17 + +## 0.6.1 + +### Patch Changes + +- acf6c8c10b: Deprecate `RadarEntry.url` - use `RadarEntry.links` instead + + ```diff + - url: 'https://www.javascript.com/', + key: 'javascript', + id: 'javascript', + title: 'JavaScript', + quadrant: 'languages', + links: [ + + { + + url: 'https://www.javascript.com/', + + title: 'Learn more', + + }, + ], + ``` + +- 18024a231c: Allow to set additional links to the entry description. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + +## 0.6.1-next.1 + +### Patch Changes + +- acf6c8c10b: Deprecate `RadarEntry.url` - use `RadarEntry.links` instead + + ```diff + - url: 'https://www.javascript.com/', + key: 'javascript', + id: 'javascript', + title: 'JavaScript', + quadrant: 'languages', + links: [ + + { + + url: 'https://www.javascript.com/', + + title: 'Learn more', + + }, + ], + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## 0.6.1-next.0 + +### Patch Changes + +- 18024a231c: Allow to set additional links to the entry description. +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.6.0 ### Minor Changes diff --git a/plugins/tech-radar/api-report.md b/plugins/tech-radar/api-report.md index 39fa19da43..3b84990d85 100644 --- a/plugins/tech-radar/api-report.md +++ b/plugins/tech-radar/api-report.md @@ -21,8 +21,16 @@ export interface RadarEntry { description?: string; id: string; key: string; + links?: Array<RadarEntryLink>; quadrant: string; timeline: Array<RadarEntrySnapshot>; + title: string; + // @deprecated + url?: string; +} + +// @public +export interface RadarEntryLink { title: string; url: string; } diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index a7ce33c427..afb484dc06 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.6.0", + "version": "0.6.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -59,7 +59,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/tech-radar/src/api.ts b/plugins/tech-radar/src/api.ts index c932ec98e2..c0b8fb8852 100644 --- a/plugins/tech-radar/src/api.ts +++ b/plugins/tech-radar/src/api.ts @@ -89,6 +89,22 @@ export interface RadarQuadrant { name: string; } +/** + * Single link in {@link RadarEntry} + * + * @public + */ +export interface RadarEntryLink { + /** + * URL of the link + */ + url: string; + /** + * Display name of the link + */ + title: string; +} + /** * Single Entry in Tech Radar * @@ -117,8 +133,10 @@ export interface RadarEntry { * @remarks * * You can use `#` if you don't want to provide any other url + * + * @deprecated Use {@link RadarEntry.links} instead */ - url: string; + url?: string; /** * History of the Entry moving through {@link RadarRing} */ @@ -127,6 +145,10 @@ export interface RadarEntry { * Description of the Entry */ description?: string; + /** + * User-clickable links to provide more information about the Entry + */ + links?: Array<RadarEntryLink>; } /** diff --git a/plugins/tech-radar/src/components/RadarComponent.tsx b/plugins/tech-radar/src/components/RadarComponent.tsx index 4ed7626d52..1f14e9d258 100644 --- a/plugins/tech-radar/src/components/RadarComponent.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.tsx @@ -126,6 +126,7 @@ export function RadarComponent(props: TechRadarComponentProps) { moved: entry.timeline[0].moved, description: entry.description || entry.timeline[0].description, url: entry.url, + links: entry.links, })); }; diff --git a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx index 7f2dcbe6ac..51fed918ec 100644 --- a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx +++ b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx @@ -24,6 +24,7 @@ const minProps: Props = { open: true, title: 'example-title', description: 'example-description', + links: [{ url: 'https://example.com/docs', title: 'example-link' }], onClose: () => {}, }; @@ -34,5 +35,6 @@ describe('RadarDescription', () => { const radarDescription = screen.getByTestId('radar-description'); expect(radarDescription).toBeInTheDocument(); expect(screen.getByText(String(minProps.description))).toBeInTheDocument(); + expect(screen.getByText(String('example-link'))).toBeInTheDocument(); }); }); diff --git a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx index 302e77c1ac..92b15d985a 100644 --- a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx +++ b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx @@ -28,10 +28,18 @@ export type Props = { title: string; description: string; url?: string; + links?: Array<{ url: string; title: string }>; }; const RadarDescription = (props: Props): JSX.Element => { - const { open, onClose, title, description, url } = props; + function showDialogActions( + url: string | undefined, + links: Array<{ url: string; title: string }> | undefined, + ): Boolean { + return isValidUrl(url) || Boolean(links && links.length > 0); + } + + const { open, onClose, title, description, url, links } = props; return ( <Dialog data-testid="radar-description" open={open} onClose={onClose}> @@ -41,17 +49,32 @@ const RadarDescription = (props: Props): JSX.Element => { <DialogContent dividers> <MarkdownContent content={description} /> </DialogContent> - {isValidUrl(url) && ( + {showDialogActions(url, links) && ( <DialogActions> - <Button - component={Link} - to={url} - onClick={onClose} - color="primary" - startIcon={<LinkIcon />} - > - LEARN MORE - </Button> + {links?.map(link => ( + <Button + component={Link} + to={link.url} + onClick={onClose} + color="primary" + startIcon={<LinkIcon />} + key={link.url} + > + {link.title} + </Button> + ))} + {isValidUrl(url) && ( + <Button + component={Link} + to={url} + onClick={onClose} + color="primary" + startIcon={<LinkIcon />} + key={url} + > + LEARN MORE + </Button> + )} </DialogActions> )} </Dialog> diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx index b0d9196e1e..7e31ddddd5 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx @@ -25,6 +25,7 @@ type RadarLegendLinkProps = { title?: string; classes: ClassNameMap<string>; active?: boolean; + links: Array<{ url: string; title: string }>; }; export const RadarLegendLink = ({ @@ -33,6 +34,7 @@ export const RadarLegendLink = ({ title, classes, active, + links, }: RadarLegendLinkProps) => { const [open, setOpen] = React.useState(false); @@ -73,6 +75,7 @@ export const RadarLegendLink = ({ title={title ? title : 'no title'} url={url} description={description} + links={links} /> )} </> diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx index 09badecdcd..68b42373f3 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx @@ -61,6 +61,7 @@ export const RadarLegendRing = ({ title={entry.title} description={entry.description} active={entry.active} + links={entry.links ?? []} /> </li> ))} diff --git a/plugins/tech-radar/src/sample.ts b/plugins/tech-radar/src/sample.ts index 174bd7750f..395b4f2ff5 100644 --- a/plugins/tech-radar/src/sample.ts +++ b/plugins/tech-radar/src/sample.ts @@ -45,11 +45,20 @@ entries.push({ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', }, ], - url: '#', key: 'javascript', id: 'javascript', title: 'JavaScript', quadrant: 'languages', + links: [ + { + url: 'https://www.javascript.com/', + title: 'Learn more', + }, + { + url: 'https://www.typescriptlang.org/', + title: 'TypeScript', + }, + ], description: 'Excepteur **sint** occaecat *cupidatat* non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\n```ts\nconst x = "3";\n```\n', }); @@ -63,7 +72,6 @@ entries.push({ 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat', }, ], - url: '#', key: 'typescript', id: 'typescript', title: 'TypeScript', @@ -81,7 +89,12 @@ entries.push({ 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur', }, ], - url: 'https://webpack.js.org/', + links: [ + { + url: 'https://webpack.js.org/', + title: 'Learn more', + }, + ], key: 'webpack', id: 'webpack', title: 'Webpack', @@ -95,7 +108,12 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: 'https://reactjs.org/', + links: [ + { + url: 'https://reactjs.org/', + title: 'Learn more', + }, + ], key: 'react', id: 'react', title: 'React', @@ -109,7 +127,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'code-reviews', id: 'code-reviews', title: 'Code Reviews', @@ -123,7 +140,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'mob-programming', id: 'mob-programming', title: 'Mob Programming', @@ -137,7 +153,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'docs-like-code', id: 'docs-like-code', title: 'Docs-like-code', @@ -150,7 +165,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'force-push', id: 'force-push', title: 'Force push to master', @@ -164,7 +178,12 @@ entries.push({ description: 'long description', }, ], - url: 'https://github.com', + links: [ + { + url: 'https://github.com', + title: 'Learn more', + }, + ], key: 'github-actions', id: 'github-actions', title: 'GitHub Actions', diff --git a/plugins/tech-radar/src/utils/types.ts b/plugins/tech-radar/src/utils/types.ts index 8689b17678..047319bd9e 100644 --- a/plugins/tech-radar/src/utils/types.ts +++ b/plugins/tech-radar/src/utils/types.ts @@ -61,6 +61,7 @@ export type Entry = { title: string; // An URL to a longer description as to why this entry is where it is url?: string; + links?: Array<{ title: string; url: string }>; // How this entry has recently moved; -1 for "down", +1 for "up", 0 for not moved moved?: MovedState; // Most recent description to display in the UI diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index 8fdcfa1715..7f8b72744e 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,82 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.11-next.0 + +### Patch Changes + +- 2b00d8a225: Fix bug/problem with test helper +- Updated dependencies + - @backstage/plugin-techdocs@1.6.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/test-utils@1.2.6-next.0 + - @backstage/plugin-catalog@1.9.0-next.0 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-search-react@1.5.1-next.0 + +## 1.0.10 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/plugin-catalog@1.8.0 + - @backstage/plugin-techdocs@1.5.0 + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/integration-react@1.1.10 + - @backstage/test-utils@1.2.5 + - @backstage/plugin-techdocs-react@1.1.3 + +## 1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## 1.0.10-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/plugin-techdocs@1.5.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + +## 1.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.4-next.0 + - @backstage/plugin-catalog@1.7.3-next.0 + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/integration-react@1.1.9 + ## 1.0.9 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index 78dfd8a17f..2f9078c7f8 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.9", + "version": "1.0.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,7 +42,7 @@ "@backstage/plugin-techdocs-react": "workspace:^", "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.9.13", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@testing-library/react": "^12.1.3", @@ -63,7 +63,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/techdocs-addons-test-utils/src/test-utils.tsx b/plugins/techdocs-addons-test-utils/src/test-utils.tsx index 0d65e7e51e..e60a96402c 100644 --- a/plugins/techdocs-addons-test-utils/src/test-utils.tsx +++ b/plugins/techdocs-addons-test-utils/src/test-utils.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ReactElement, Fragment } from 'react'; +import React, { ReactElement } from 'react'; // Shadow DOM support for the simple and complete DOM testing utilities // https://github.com/testing-library/dom-testing-library/issues/742#issuecomment-674987855 @@ -236,9 +236,9 @@ export class TechDocsAddonTester { element={<TechDocsReaderPage />} > <TechDocsAddons> - {this.addons.map((addon, index) => ( - <Fragment key={index}>{addon}</Fragment> - ))} + {this.addons.map((addon, index) => + React.cloneElement(addon, { key: index }), + )} </TechDocsAddons> </Route> </FlatRoutes> diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index a2e5f4d02b..4ba0a36a48 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-techdocs-backend +## 1.5.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.12-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-node@1.5.1-next.0 + +## 1.5.3 + +### Patch Changes + +- 6f097023fc: Keep the tech docs sync event stream alive even if it is taking a while to build. +- 339d9a5b5c: Added support for using a default `mkdocs.yml` configuration file when none is provided +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/plugin-techdocs-node@1.5.0 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-common@1.0.11 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## 1.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-node@1.4.6-next.2 + +## 1.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-common@1.0.11-next.0 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-node@1.4.6-next.1 + +## 1.5.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.4.6-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/plugin-catalog-common@1.0.11-next.0 + ## 1.5.1 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 9760707736..5964e3f58f 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "1.5.1", + "version": "1.5.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -59,7 +59,7 @@ "@backstage/cli": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@types/dockerode": "^3.3.0", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 09eee1fc12..b4c2d2ee26 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -186,6 +186,9 @@ export class DocsBuilder { etag: newEtag, logger: this.logger, logStream: this.logStream, + siteOptions: { + name: this.entity.metadata.title ?? this.entity.metadata.name, + }, }); // Remove Prepared directory since it is no longer needed. diff --git a/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts b/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts index 039f1db09f..fa4c81682c 100644 --- a/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts +++ b/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts @@ -31,6 +31,7 @@ describe('TechDocsCache', () => { get: jest.fn(), set: jest.fn(), delete: jest.fn(), + withOptions: jest.fn(), }; CacheUnderTest = TechDocsCache.fromConfig(new ConfigReader({}), { cache: MockClient, diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index a76ea66729..86ab9e6cd8 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -29,7 +29,7 @@ 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 { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha'; import { Permission } from '@backstage/plugin-permission-common'; import { CatalogApi, diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts index aaddb4fa51..e2c6f5913c 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts @@ -30,7 +30,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha'; import { Permission } from '@backstage/plugin-permission-common'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { TechDocsDocument } from '@backstage/plugin-techdocs-node'; diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts index 1445079d12..52a3b82651 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts @@ -32,6 +32,7 @@ import { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; jest.mock('../DocsBuilder'); +jest.useFakeTimers(); jest.mock('node-fetch', () => ({ __esModule: true, @@ -130,6 +131,8 @@ describe('DocsSynchronizer', () => { const logger = MockedDocsBuilder.mock.calls[0][0].logger; + jest.advanceTimersByTime(10001); + logger.info('Some more log'); return true; @@ -144,13 +147,19 @@ describe('DocsSynchronizer', () => { generators, }); - expect(mockResponseHandler.log).toHaveBeenCalledTimes(3); + expect(mockResponseHandler.log).toHaveBeenCalledTimes(4); expect(mockResponseHandler.log).toHaveBeenCalledWith('Some log'); expect(mockResponseHandler.log).toHaveBeenCalledWith('Another log'); expect(mockResponseHandler.log).toHaveBeenCalledWith( expect.stringMatching(/info.*Some more log/), ); + expect(mockResponseHandler.log).toHaveBeenCalledWith( + expect.stringMatching( + /info.*The docs building process is taking a little bit longer to process this entity. Please bear with us/, + ), + ); + expect(mockResponseHandler.finish).toHaveBeenCalledWith({ updated: true, }); diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 647447a612..8456f64a6a 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -128,7 +128,13 @@ export class DocsSynchronizer { cache: this.cache, }); + const interval = setInterval(() => { + taskLogger.info( + 'The docs building process is taking a little bit longer to process this entity. Please bear with us.', + ); + }, 10000); const updated = await this.buildLimiter(() => docsBuilder.build()); + clearInterval(interval); if (!updated) { finish({ updated: false }); @@ -159,7 +165,7 @@ export class DocsSynchronizer { ); error( new NotFoundError( - 'Sorry! It took too long for the generated docs to show up in storage. Check back later.', + 'Sorry! It took too long for the generated docs to show up in storage. Are you sure the docs project is generating an `index.html` file? Otherwise, check back later.', ), ); return; diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index 1608822d2b..abe7bc542f 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,94 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.0.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + +## 1.0.10 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- 4660b63947: Create a TechDocs `<LightBox/>` addon that allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. + + Here's an example on how to use it in a Backstage app: + + ```diff + import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, + } from '@backstage/plugin-techdocs'; + import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha'; + +import { LightBox } from '@backstage/plugin-techdocs-module-addons-contrib'; + + const AppRoutes = () => { + <FlatRoutes> + // other plugin routes + <Route path="/docs" element={<TechDocsIndexPage />}> + <DefaultTechDocsHome /> + </Route> + <Route + path="/docs/:namespace/:kind/:name/*" + element={<TechDocsReaderPage />} + > + <TechDocsAddons> + + <LightBox /> + </TechDocsAddons> + </Route> + </FlatRoutes>; + }; + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-techdocs-react@1.1.3 + +## 1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## 1.0.10-next.1 + +### Patch Changes + +- d950d3e217: Depend on `@material-ui/core` version `^4.12.2` like all other in-repo packages +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + +## 1.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/integration-react@1.1.9 + ## 1.0.9 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/api-report.md b/plugins/techdocs-module-addons-contrib/api-report.md index 68fad9121d..c242bb02f2 100644 --- a/plugins/techdocs-module-addons-contrib/api-report.md +++ b/plugins/techdocs-module-addons-contrib/api-report.md @@ -10,6 +10,9 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; // @public export const ExpandableNavigation: () => JSX.Element | null; +// @public +export const LightBox: () => JSX.Element | null; + // @public export const ReportIssue: (props: ReportIssueProps) => JSX.Element | null; diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 984cd02238..51d656007d 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", "description": "Plugin module for contributed TechDocs Addons", - "version": "1.0.9", + "version": "1.0.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,11 +39,12 @@ "@backstage/integration-react": "workspace:^", "@backstage/plugin-techdocs-react": "workspace:^", "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.9.13", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@react-hookz/web": "^20.0.0", "git-url-parse": "^13.0.0", + "photoswipe": "^5.3.5", "react-use": "^17.2.4" }, "peerDependencies": { @@ -61,7 +62,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.test.tsx b/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.test.tsx new file mode 100644 index 0000000000..b63aed8777 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils'; + +import React from 'react'; + +import { LightBox } from '../plugin'; + +describe('LightBox', () => { + it('renders without exploding', async () => { + const { getByText } = await TechDocsAddonTester.buildAddonsInTechDocs([ + <LightBox />, + ]) + .withDom(<body>TEST_CONTENT</body>) + .renderWithEffects(); + + expect(getByText('TEST_CONTENT')).toBeInTheDocument(); + }); + + it('Add onclick event to images', async () => { + const { getByTestId } = await TechDocsAddonTester.buildAddonsInTechDocs([ + <LightBox />, + ]) + .withDom( + <img + data-testid="fixture" + src="http://example.com/dog.jpg" + alt="dog" + />, + ) + .renderWithEffects(); + + expect(getByTestId('fixture').onclick).not.toBeUndefined(); + expect(getByTestId('fixture').onclick).toEqual(expect.any(Function)); + }); +}); diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.tsx b/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.tsx new file mode 100644 index 0000000000..b74c2f9659 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.tsx @@ -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 { useEffect } from 'react'; +import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; +// @ts-ignore +import PhotoSwipeLightbox, { DataSource } from 'photoswipe/lightbox'; +import PhotoSwipe from 'photoswipe'; +import 'photoswipe/style.css'; +import './lightbox.css'; + +export const LightBoxAddon = () => { + const images = useShadowRootElements<HTMLImageElement>(['img']); + + useEffect(() => { + let dataSourceImages: DataSource | null = null; + + let lightbox = new PhotoSwipeLightbox({ + pswpModule: PhotoSwipe, + wheelToZoom: true, + arrowPrevSVG: + '<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-c1sh5i" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="ArrowBackIosIcon" aria-label="fontSize large"><path d="M11.67 3.87 9.9 2.1 0 12l9.9 9.9 1.77-1.77L3.54 12z"></path></svg>', + arrowNextSVG: + '<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-c1sh5i" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="ArrowForwardIosIcon" aria-label="fontSize large"><path d="M6.23 20.23 8 22l10-10L8 2 6.23 3.77 14.46 12z"></path></svg>', + closeSVG: + '<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-c1sh5i" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="CloseIcon" aria-label="fontSize large"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg>', + zoomSVG: + '<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge css-c1sh5i" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="ZoomInIcon" aria-label="fontSize large"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path><path d="M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z"></path></svg>', + }); + + images.forEach((image, index) => { + image.onclick = () => { + if (dataSourceImages === null) { + dataSourceImages = images.map(dataSourceImage => { + return { + element: dataSourceImage, + src: dataSourceImage.src, + msrc: dataSourceImage.src, + alt: dataSourceImage.alt, + width: dataSourceImage.clientWidth, + height: dataSourceImage.clientHeight, + }; + }); + } + lightbox.loadAndOpen(index, dataSourceImages); + return false; + }; + }); + lightbox.init(); + + return () => { + lightbox.destroy(); + lightbox = null; + }; + }, [images]); + + return null; +}; diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/index.ts b/plugins/techdocs-module-addons-contrib/src/LigthBox/index.ts new file mode 100644 index 0000000000..144359abb7 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/LigthBox/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './LightBox'; diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css b/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css new file mode 100644 index 0000000000..63170792f1 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css @@ -0,0 +1,7 @@ +.pswp { + --pswp-bg: #f8f8f8; +} +.pswp__counter { + color: rgba(0, 0, 0, 0.87); + text-shadow: none; +} diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx index 5fff56ecb7..9a34fcf011 100644 --- a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx @@ -26,7 +26,7 @@ import { import { IssueLink } from './IssueLink'; -const defaultProps = { +const defaultGithubProps = { repository: { type: 'github', name: 'backstage', @@ -40,14 +40,28 @@ const defaultProps = { }, }; +const defaultGitlabProps = { + repository: { + type: 'gitlab', + name: 'backstageSubgroup/backstage', + owner: 'backstage', + protocol: 'https', + resource: 'gitlab.com', + }, + template: { + title: 'Documentation feedback', + body: '## Documentation Feedback 📝', + }, +}; + describe('FeedbackLink', () => { const apiSpy = new MockAnalyticsApi(); - it('Should open new issue tab', () => { + it('Should open new Github issue tab', () => { render( wrapInTestApp( <TestApiProvider apis={[[analyticsApiRef, apiSpy]]}> - <IssueLink {...defaultProps} /> + <IssueLink {...defaultGithubProps} /> </TestApiProvider>, ), ); @@ -55,19 +69,39 @@ describe('FeedbackLink', () => { const link = screen.getByText(/Open new Github issue/); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute('target', '_blank'); - const encodedTitle = encodeURIComponent(defaultProps.template.title); - const encodedBody = encodeURIComponent(defaultProps.template.body); + const encodedTitle = encodeURIComponent(defaultGithubProps.template.title); + const encodedBody = encodeURIComponent(defaultGithubProps.template.body); expect(link).toHaveAttribute( 'href', `https://github.com/backstage/backstage/issues/new?title=${encodedTitle}&body=${encodedBody}`, ); }); + it('Should open new Gitlab issue tab', () => { + render( + wrapInTestApp( + <TestApiProvider apis={[[analyticsApiRef, apiSpy]]}> + <IssueLink {...defaultGitlabProps} /> + </TestApiProvider>, + ), + ); + + const link = screen.getByText(/Open new Gitlab issue/); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('target', '_blank'); + const encodedTitle = encodeURIComponent(defaultGithubProps.template.title); + const encodedBody = encodeURIComponent(defaultGithubProps.template.body); + expect(link).toHaveAttribute( + 'href', + `https://gitlab.com/backstage/backstageSubgroup/backstage/issues/new?issue[title]=${encodedTitle}&issue[description]=${encodedBody}`, + ); + }); + it('Should track click events', async () => { render( wrapInTestApp( <TestApiProvider apis={[[analyticsApiRef, apiSpy]]}> - <IssueLink {...defaultProps} /> + <IssueLink {...defaultGithubProps} /> </TestApiProvider>, ), ); diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx index bb2de22350..eb16476fa6 100644 --- a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx @@ -59,14 +59,13 @@ const getUrl = (repository: Repository, template: ReportIssueTemplate) => { const encodedTitle = encodeURIComponent(title); const encodedBody = encodeURIComponent(body); const { protocol, resource, owner, name, type } = repository; - const encodedOwner = encodeURIComponent(owner); - const encodedName = encodeURIComponent(name); - const url = `${protocol}://${resource}/${encodedOwner}/${encodedName}`; + const url = `${protocol}://${resource}/${owner}/${name}`; + const encodedUrl = encodeURI(url); if (type === 'github') { - return `${url}/issues/new?title=${encodedTitle}&body=${encodedBody}`; + return `${encodedUrl}/issues/new?title=${encodedTitle}&body=${encodedBody}`; } - return `${url}/issues/new?issue[title]=${encodedTitle}&issue[description]=${encodedBody}`; + return `${encodedUrl}/issues/new?issue[title]=${encodedTitle}&issue[description]=${encodedBody}`; }; export const IssueLink = ({ template, repository }: IssueLinkProps) => { diff --git a/plugins/techdocs-module-addons-contrib/src/index.ts b/plugins/techdocs-module-addons-contrib/src/index.ts index 528afe5c0d..bad7701486 100644 --- a/plugins/techdocs-module-addons-contrib/src/index.ts +++ b/plugins/techdocs-module-addons-contrib/src/index.ts @@ -25,6 +25,7 @@ export { ExpandableNavigation, ReportIssue, TextSize, + LightBox, } from './plugin'; export type { ReportIssueProps, diff --git a/plugins/techdocs-module-addons-contrib/src/plugin.ts b/plugins/techdocs-module-addons-contrib/src/plugin.ts index 6136f176d0..c4023a72af 100644 --- a/plugins/techdocs-module-addons-contrib/src/plugin.ts +++ b/plugins/techdocs-module-addons-contrib/src/plugin.ts @@ -22,6 +22,7 @@ import { import { ExpandableNavigationAddon } from './ExpandableNavigation'; import { ReportIssueAddon, ReportIssueProps } from './ReportIssue'; import { TextSizeAddon } from './TextSize'; +import { LightBoxAddon } from './LigthBox'; /** * The TechDocs addons contrib plugin @@ -203,3 +204,49 @@ export const TextSize = techdocsModuleAddonsContribPlugin.provide( component: TextSizeAddon, }), ); + +/** + * This TechDocs addon allows users to open images in a lightbox on documentation pages, they can navigate between images if there are several on one page. + * + * @remarks + * The image size of the lightbox image is the same as the image size on the document page. + * + * @example + * Here's a simple example: + * ``` + * import { + * DefaultTechDocsHome, + * TechDocsIndexPage, + * TechDocsReaderPage, + * } from '@backstage/plugin-techdocs'; + * import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; + * import { LightBox } from '@backstage/plugin-techdocs-module-addons-contrib'; + * + * + * const AppRoutes = () => { + * <FlatRoutes> + * // other plugin routes + * <Route path="/docs" element={<TechDocsIndexPage />}> + * <DefaultTechDocsHome /> + * </Route> + * <Route + * path="/docs/:namespace/:kind/:name/*" + * element={<TechDocsReaderPage />} + * > + * <TechDocsAddons> + * <LightBox /> + * </TechDocsAddons> + * </Route> + * </FlatRoutes>; + * }; + * ``` + * + * @public + */ +export const LightBox = techdocsModuleAddonsContribPlugin.provide( + createTechDocsAddonExtension({ + name: 'LightBox', + location: TechDocsAddonLocations.Content, + component: LightBoxAddon, + }), +); diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index 45e5659988..4c14489d5a 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-techdocs-node +## 1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + +## 1.5.0 + +### Minor Changes + +- dd1e37649f: Deprecated getMkDocsYml in favor of getMkdocsYml (lowercase 'd') + +### Patch Changes + +- 87ab76e55c: Fixed bug caused by recent migration to AWS SDK V3 for techdocs-node. Instead of s3ForcePathStyle, forcePathStyle should be passed. +- 339d9a5b5c: Added support for using a default `mkdocs.yml` configuration file when none is provided +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + +## 1.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + +## 1.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + +## 1.4.6-next.0 + +### Patch Changes + +- 87ab76e55c: Fixed bug caused by recent migration to AWS SDK V3 for techdocs-node. Instead of s3ForcePathStyle, forcePathStyle should be passed. +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + ## 1.4.4 ### Patch Changes diff --git a/plugins/techdocs-node/api-report.md b/plugins/techdocs-node/api-report.md index e27b406478..11ff53514b 100644 --- a/plugins/techdocs-node/api-report.md +++ b/plugins/techdocs-node/api-report.md @@ -54,6 +54,9 @@ export type GeneratorRunOptions = { etag?: string; logger: Logger; logStream?: Writable; + siteOptions?: { + name?: string; + }; }; // @public @@ -85,6 +88,32 @@ export const getLocationForEntity: ( scmIntegration: ScmIntegrationRegistry, ) => ParsedLocationAnnotation; +// @public @deprecated (undocumented) +export const getMkDocsYml: ( + inputDir: string, + siteOptions?: + | { + name?: string | undefined; + } + | undefined, +) => Promise<{ + path: string; + content: string; + configIsTemporary: boolean; +}>; + +// @public +export const getMkdocsYml: ( + inputDir: string, + siteOptions?: { + name?: string; + }, +) => Promise<{ + path: string; + content: string; + configIsTemporary: boolean; +}>; + // @public export type MigrateRequest = { removeOriginal?: boolean; diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 47a9eda22b..2b9e74a359 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-node", "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "1.4.4", + "version": "1.5.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -42,6 +42,7 @@ "@aws-sdk/client-s3": "^3.208.0", "@aws-sdk/credential-providers": "^3.208.0", "@aws-sdk/lib-storage": "^3.208.0", + "@aws-sdk/node-http-handler": "^3.208.0", "@aws-sdk/types": "^3.208.0", "@azure/identity": "^2.1.0", "@azure/storage-blob": "^12.5.0", @@ -58,6 +59,7 @@ "express": "^4.17.1", "fs-extra": "10.1.0", "git-url-parse": "^13.0.0", + "hpagent": "^1.2.0", "js-yaml": "^4.0.0", "json5": "^2.1.3", "mime-types": "^2.1.27", diff --git a/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_default.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_default.yml new file mode 100644 index 0000000000..460e5c0e1d --- /dev/null +++ b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_default.yml @@ -0,0 +1,4 @@ +site_name: Default Test site name +docs_dir: docs +plugins: + - techdocs-core diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index e894b0602e..b24d85430b 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -34,7 +34,7 @@ import { import { patchMkdocsYmlPreBuild, pathMkdocsYmlWithTechdocsPlugin, -} from './mkDocsPatchers'; +} from './mkdocsPatchers'; import yaml from 'js-yaml'; const mockEntity = { @@ -42,12 +42,16 @@ const mockEntity = { kind: 'TestKind', metadata: { name: 'testName', + title: 'Test site name', }, }; const mkdocsYml = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs.yml'), ); +const mkdocsDefaultYml = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_default.yml'), +); const mkdocsYmlWithExtensions = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_extensions.yml'), ); @@ -186,6 +190,7 @@ describe('helpers', () => { beforeEach(() => { mockFs({ '/mkdocs.yml': mkdocsYml, + '/mkdocs_default.yml': mkdocsDefaultYml, '/mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl, '/mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri, '/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions, @@ -514,28 +519,60 @@ describe('helpers', () => { describe('getMkdocsYml', () => { const inputDir = resolvePath(__filename, '../__fixtures__/'); + const siteOptions = { + name: mockEntity.metadata.title, + }; it('returns expected contents when .yml file is present', async () => { const key = path.join(inputDir, 'mkdocs.yml'); mockFs({ [key]: mkdocsYml }); - const { path: mkdocsPath, content } = await getMkdocsYml(inputDir); + const { + path: mkdocsPath, + content, + configIsTemporary, + } = await getMkdocsYml(inputDir, siteOptions); expect(mkdocsPath).toBe(key); expect(content).toBe(mkdocsYml.toString()); + expect(configIsTemporary).toBe(false); }); it('returns expected contents when .yaml file is present', async () => { const key = path.join(inputDir, 'mkdocs.yaml'); mockFs({ [key]: mkdocsYml }); - const { path: mkdocsPath, content } = await getMkdocsYml(inputDir); + const { + path: mkdocsPath, + content, + configIsTemporary, + } = await getMkdocsYml(inputDir, siteOptions); expect(mkdocsPath).toBe(key); expect(content).toBe(mkdocsYml.toString()); + expect(configIsTemporary).toBe(false); }); - it('throws when neither .yml nor .yaml file is present', async () => { + it('returns expected contents when default file is present', async () => { + const defaultSiteOptions = { + name: 'Default Test site name', + }; + const key = path.join(inputDir, 'mkdocs.yml'); + const mockPathExists = jest.spyOn(fs, 'pathExists'); + mockPathExists.mockImplementation(() => Promise.resolve(false)); + mockFs({ [key]: mkdocsDefaultYml }); + const { + path: mkdocsPath, + content, + configIsTemporary, + } = await getMkdocsYml(inputDir, defaultSiteOptions); + + expect(mkdocsPath).toBe(key); + expect(content).toBe(mkdocsDefaultYml.toString()); + expect(configIsTemporary).toBe(true); + }); + + it('throws when neither .yml nor .yaml nor default file is present', async () => { const invalidInputDir = resolvePath(__filename); - await expect(getMkdocsYml(invalidInputDir)).rejects.toThrow( - /Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml for validation/, + await expect(getMkdocsYml(invalidInputDir, siteOptions)).rejects.toThrow( + /Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml or default for validation/, ); }); }); diff --git a/plugins/techdocs-node/src/stages/generate/helpers.ts b/plugins/techdocs-node/src/stages/generate/helpers.ts index 50db0eb86a..e16293f906 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.ts @@ -26,7 +26,7 @@ import path, { resolve as resolvePath } from 'path'; import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; import { ParsedLocationAnnotation } from '../../helpers'; -import { SupportedGeneratorKey } from './types'; +import { DefaultMkdocsContent, SupportedGeneratorKey } from './types'; import { getFileTreeRecursively } from '../publish/helpers'; // TODO: Implement proper support for more generators. @@ -150,36 +150,90 @@ export const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([ }), ]); +/** + * Generates a mkdocs.yml configuration file + * + * @param inputDir - base dir to where the mkdocs.yml file will be created + * @param siteOptions - options for the site: `name` property will be used in mkdocs.yml for the + * required `site_name` property, default value is "Documentation Site" + */ +export const generateMkdocsYml = async ( + inputDir: string, + siteOptions?: { name?: string }, +) => { + try { + // TODO(awanlin): Use a provided default mkdocs.yml + // from config or some specified location. If this is + // not provided then fall back to generating bare + // minimum mkdocs.yml file + + const mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml'); + const defaultSiteName = siteOptions?.name ?? 'Documentation Site'; + const defaultMkdocsContent: DefaultMkdocsContent = { + site_name: defaultSiteName, + docs_dir: 'docs', + plugins: ['techdocs-core'], + }; + + await fs.writeFile( + mkdocsYmlPath, + yaml.dump(defaultMkdocsContent, { schema: MKDOCS_SCHEMA }), + ); + } catch (error) { + throw new ForwardedError('Could not generate mkdocs.yml file', error); + } +}; + /** * Finds and loads the contents of either an mkdocs.yml or mkdocs.yaml file, * depending on which is present (MkDocs supports both as of v1.2.2). + * @public * - * @param inputDir - base dir to be searched for either an mkdocs.yml or - * mkdocs.yaml file. + * @param inputDir - base dir to be searched for either an mkdocs.yml or mkdocs.yaml file. + * @param siteOptions - options for the site: `name` property will be used in mkdocs.yml for the + * required `site_name` property, default value is "Documentation Site" */ export const getMkdocsYml = async ( inputDir: string, -): Promise<{ path: string; content: string }> => { + siteOptions?: { name?: string }, +): Promise<{ path: string; content: string; configIsTemporary: boolean }> => { let mkdocsYmlPath: string; let mkdocsYmlFileString: string; try { mkdocsYmlPath = path.join(inputDir, 'mkdocs.yaml'); - mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); - } catch { - try { - mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml'); + if (await fs.pathExists(mkdocsYmlPath)) { mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); - } catch (error) { - throw new ForwardedError( - 'Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml for validation', - error, - ); + return { + path: mkdocsYmlPath, + content: mkdocsYmlFileString, + configIsTemporary: false, + }; } + + mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml'); + if (await fs.pathExists(mkdocsYmlPath)) { + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + return { + path: mkdocsYmlPath, + content: mkdocsYmlFileString, + configIsTemporary: false, + }; + } + + // No mkdocs file, generate it + await generateMkdocsYml(inputDir, siteOptions); + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + } catch (error) { + throw new ForwardedError( + 'Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml or default for validation', + error, + ); } return { path: mkdocsYmlPath, content: mkdocsYmlFileString, + configIsTemporary: true, }; }; diff --git a/plugins/techdocs-node/src/stages/generate/index.ts b/plugins/techdocs-node/src/stages/generate/index.ts index 1c20c58887..aaf27026c2 100644 --- a/plugins/techdocs-node/src/stages/generate/index.ts +++ b/plugins/techdocs-node/src/stages/generate/index.ts @@ -15,6 +15,7 @@ */ export { TechdocsGenerator } from './techdocs'; export { Generators } from './generators'; +export { getMkdocsYml } from './helpers'; export type { GeneratorBase, GeneratorOptions, @@ -22,3 +23,10 @@ export type { GeneratorRunOptions, SupportedGeneratorKey, } from './types'; +import { getMkdocsYml } from './helpers'; +/** + * @public + * @deprecated + * Deprecated in favor of getMkdocsYml (lowercase 'd') + */ +export const getMkDocsYml = getMkdocsYml; diff --git a/plugins/techdocs-node/src/stages/generate/mkDocsPatchers.ts b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts similarity index 100% rename from plugins/techdocs-node/src/stages/generate/mkDocsPatchers.ts rename to plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts diff --git a/plugins/techdocs-node/src/stages/generate/techdocs.ts b/plugins/techdocs-node/src/stages/generate/techdocs.ts index 9bbc58ebdc..2f88e1a8ad 100644 --- a/plugins/techdocs-node/src/stages/generate/techdocs.ts +++ b/plugins/techdocs-node/src/stages/generate/techdocs.ts @@ -34,7 +34,7 @@ import { import { patchMkdocsYmlPreBuild, pathMkdocsYmlWithTechdocsPlugin, -} from './mkDocsPatchers'; +} from './mkdocsPatchers'; import { GeneratorBase, GeneratorConfig, @@ -96,10 +96,14 @@ export class TechdocsGenerator implements GeneratorBase { etag, logger: childLogger, logStream, + siteOptions, } = options; // Do some updates to mkdocs.yml before generating docs e.g. adding repo_url - const { path: mkdocsYmlPath, content } = await getMkdocsYml(inputDir); + const { path: mkdocsYmlPath, content } = await getMkdocsYml( + inputDir, + siteOptions, + ); // validate the docs_dir first const docsDir = await validateMkdocsYaml(inputDir, content); diff --git a/plugins/techdocs-node/src/stages/generate/types.ts b/plugins/techdocs-node/src/stages/generate/types.ts index 2b2bbb21c9..70c332344a 100644 --- a/plugins/techdocs-node/src/stages/generate/types.ts +++ b/plugins/techdocs-node/src/stages/generate/types.ts @@ -53,6 +53,7 @@ export type GeneratorConfig = { * @param etag - A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. * @param logger - A logger that forwards the messages to the caller to be displayed outside of the backend. * @param logStream - A log stream that can send raw log messages to the caller to be displayed outside of the backend. + * @param siteOptions - Options for the site: The `name` property will be used in mkdocs.yml config for the required `site_name` property, default value is "Documentation Site" */ export type GeneratorRunOptions = { inputDir: string; @@ -61,6 +62,7 @@ export type GeneratorRunOptions = { etag?: string; logger: Logger; logStream?: Writable; + siteOptions?: { name?: string }; }; /** @@ -89,3 +91,9 @@ export type GeneratorBuilder = { register(protocol: SupportedGeneratorKey, generator: GeneratorBase): void; get(entity: Entity): GeneratorBase; }; + +export type DefaultMkdocsContent = { + site_name: string; + docs_dir: string; + plugins: String[]; +}; diff --git a/plugins/techdocs-node/src/stages/publish/awsS3.ts b/plugins/techdocs-node/src/stages/publish/awsS3.ts index a13baef926..679e9bf7c9 100644 --- a/plugins/techdocs-node/src/stages/publish/awsS3.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.ts @@ -32,8 +32,10 @@ import { S3Client, } from '@aws-sdk/client-s3'; import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; +import { NodeHttpHandler } from '@aws-sdk/node-http-handler'; import { Upload } from '@aws-sdk/lib-storage'; import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; +import { HttpsProxyAgent } from 'hpagent'; import express from 'express'; import fs from 'fs-extra'; import JSON5 from 'json5'; @@ -150,9 +152,14 @@ export class AwsS3Publish implements PublisherBase { 'techdocs.publisher.awsS3.endpoint', ); + // AWS HTTPS proxy is an optional config. If missing, no proxy is used + const httpsProxy = config.getOptionalString( + 'techdocs.publisher.awsS3.httpsProxy', + ); + // AWS forcePathStyle is an optional config. If missing, it defaults to false. Needs to be enabled for cases // where endpoint url points to locally hosted S3 compatible storage like Localstack - const s3ForcePathStyle = config.getOptionalBoolean( + const forcePathStyle = config.getOptionalBoolean( 'techdocs.publisher.awsS3.s3ForcePathStyle', ); @@ -161,7 +168,12 @@ export class AwsS3Publish implements PublisherBase { credentialDefaultProvider: () => sdkCredentialProvider, ...(region && { region }), ...(endpoint && { endpoint }), - ...(s3ForcePathStyle && { s3ForcePathStyle }), + ...(forcePathStyle && { forcePathStyle }), + ...(httpsProxy && { + requestHandler: new NodeHttpHandler({ + httpsAgent: new HttpsProxyAgent({ proxy: httpsProxy }), + }), + }), }); const legacyPathCasing = diff --git a/plugins/techdocs-node/src/stages/publish/helpers.test.ts b/plugins/techdocs-node/src/stages/publish/helpers.test.ts index fa934d6dd9..7b303b3bf5 100644 --- a/plugins/techdocs-node/src/stages/publish/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/publish/helpers.test.ts @@ -158,6 +158,16 @@ describe('getStaleFiles', () => { expect(staleFiles).toHaveLength(1); expect(staleFiles).toEqual(expect.arrayContaining(['stale_file.png'])); }); + + it('should not return directories as stale files if they are parent directories of new files', () => { + const oldFiles = [...defaultFiles, 'default/Component/backstage/foo']; + const newFiles = [ + ...defaultFiles, + 'default/Component/backstage/foo/bar/index.html', + ]; + const staleFiles = getStaleFiles(newFiles, oldFiles); + expect(staleFiles).toHaveLength(0); + }); }); describe('getCloudPathForLocalPath', () => { diff --git a/plugins/techdocs-node/src/stages/publish/helpers.ts b/plugins/techdocs-node/src/stages/publish/helpers.ts index decefae187..0b9384dba3 100644 --- a/plugins/techdocs-node/src/stages/publish/helpers.ts +++ b/plugins/techdocs-node/src/stages/publish/helpers.ts @@ -169,8 +169,21 @@ export const getStaleFiles = ( oldFiles: string[], ): string[] => { const staleFiles = new Set(oldFiles); + const removedParentDirs = new Set(); newFiles.forEach(newFile => { staleFiles.delete(newFile); + + // We have to traverse through the directory hierarchy of a new file and + // ensure that we won't try to delete one of the parent directories. + let parentDir = newFile.substring(0, newFile.lastIndexOf('/')); + while ( + !removedParentDirs.has(parentDir) && + parentDir.length >= newFile.indexOf('/') + ) { + staleFiles.delete(parentDir); + removedParentDirs.add(parentDir); + parentDir = parentDir.substring(0, parentDir.lastIndexOf('/')); + } }); return Array.from(staleFiles); }; diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index d70252571f..fdaba0ebe4 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/plugin-techdocs-react +## 1.1.4-next.0 + +### Patch Changes + +- 928a12a9b3: Internal refactor of `/alpha` exports. +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/version-bridge@1.0.3 + +## 1.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/config@1.0.6 + - @backstage/version-bridge@1.0.3 + +## 1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/version-bridge@1.0.3 + +## 1.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/version-bridge@1.0.3 + +## 1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + ## 1.1.2 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index 2f3aefce74..e2e45febf0 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,10 +1,9 @@ { "name": "@backstage/plugin-techdocs-react", "description": "Shared frontend utilities for TechDocs and Addons", - "version": "1.1.2", + "version": "1.1.4-next.0", "publishConfig": { "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, @@ -25,7 +24,7 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -42,7 +41,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.11.0", - "jss": "~10.9.0", + "jss": "~10.10.0", "lodash": "^4.17.21", "react-helmet": "6.1.0", "react-use": "^17.2.4" diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index ad6e76c540..01a028f605 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,145 @@ # @backstage/plugin-techdocs +## 1.6.0-next.0 + +### Minor Changes + +- 3f75b7607c: Add ability to pass icon as function to have ability to customize it by search item + +### Patch Changes + +- f320c299c6: The HTML tag attributes in the documentation content inserted to shadow DOM is preserved to improve accessibility +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.0 + - @backstage/config@1.0.6 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.11-next.0 + - @backstage/theme@0.2.17 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-search-react@1.5.1-next.0 + +## 1.5.0 + +### Minor Changes + +- 20840b36b4: Update DocsTable and EntityListDocsTable to accept overrides for Material Table options. +- 0eaa579f89: The `TechDocsSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- c8e09cc383: Fixed bug in Techdocs reader where a techdocs page with a hash in the URL did not always jump to the document anchor. +- cad5607411: Improve view: remove footer overlay on large screen +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- 4660b63947: Create a TechDocs `<LightBox/>` addon that allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. + + Here's an example on how to use it in a Backstage app: + + ```diff + import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, + } from '@backstage/plugin-techdocs'; + import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha'; + +import { LightBox } from '@backstage/plugin-techdocs-module-addons-contrib'; + + const AppRoutes = () => { + <FlatRoutes> + // other plugin routes + <Route path="/docs" element={<TechDocsIndexPage />}> + <DefaultTechDocsHome /> + </Route> + <Route + path="/docs/:namespace/:kind/:name/*" + element={<TechDocsReaderPage />} + > + <TechDocsAddons> + + <LightBox /> + </TechDocsAddons> + </Route> + </FlatRoutes>; + }; + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/plugin-search-react@1.5.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-react@1.1.3 + +## 1.5.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## 1.5.0-next.1 + +### Minor Changes + +- 20840b36b4: Update DocsTable and EntityListDocsTable to accept overrides for Material Table options. +- 0eaa579f89: The `TechDocsSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-search-react@1.5.0-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-react@1.1.3-next.1 + +## 1.4.4-next.0 + +### Patch Changes + +- c8e09cc383: Fixed bug in Techdocs reader where a techdocs page with a hash in the URL did not always jump to the document anchor. +- cad5607411: Improve view: remove footer overlay on large screen +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/plugin-techdocs-react@1.1.3-next.0 + - @backstage/integration-react@1.1.9 + ## 1.4.3 ### Patch Changes diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index d1c05f5b8e..bb59732021 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -19,7 +19,9 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; +import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react'; import { TableColumn } from '@backstage/core-components'; +import { TableOptions } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; import { TechDocsEntityMetadata as TechDocsEntityMetadata_2 } from '@backstage/plugin-techdocs-react'; import { TechDocsMetadata as TechDocsMetadata_2 } from '@backstage/plugin-techdocs-react'; @@ -100,6 +102,7 @@ export type DocsTableProps = { loading?: boolean | undefined; columns?: TableColumn<DocsTableRow>[]; actions?: TableProps<DocsTableRow>['actions']; + options?: TableOptions<DocsTableRow>; }; // @public @@ -159,6 +162,7 @@ export const EntityListDocsTable: { export type EntityListDocsTableProps = { columns?: TableColumn<DocsTableRow>[]; actions?: TableProps<DocsTableRow>['actions']; + options?: TableOptions<DocsTableRow>; }; // @public @@ -397,13 +401,13 @@ export type TechDocsSearchProps = { // @public export const TechDocsSearchResultListItem: ( - props: TechDocsSearchResultListItemProps, -) => JSX.Element; + props: SearchResultListItemExtensionProps<TechDocsSearchResultListItemProps>, +) => JSX.Element | null; // @public export type TechDocsSearchResultListItemProps = { - icon?: ReactNode; - result: any; + icon?: ReactNode | ((result: any) => ReactNode); + result?: any; highlight?: ResultHighlight; rank?: number; lineClamp?: number; diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 33c5ec8852..b66750991b 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "1.4.3", + "version": "1.6.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -52,7 +52,7 @@ "dompurify": "^2.2.9", "event-source-polyfill": "1.0.25", "git-url-parse": "^13.0.0", - "jss": "~10.9.0", + "jss": "~10.10.0", "lodash": "^4.17.21", "react-helmet": "6.1.0", "react-use": "^17.2.4" @@ -78,7 +78,7 @@ "@types/node": "^16.11.26", "canvas": "^2.10.2", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/techdocs/src/home/components/Grids/DocsCardGrid.tsx b/plugins/techdocs/src/home/components/Grids/DocsCardGrid.tsx index 6f1d9fbe61..8843249291 100644 --- a/plugins/techdocs/src/home/components/Grids/DocsCardGrid.tsx +++ b/plugins/techdocs/src/home/components/Grids/DocsCardGrid.tsx @@ -19,7 +19,7 @@ import { toLowerMaybe } from '../../../helpers'; import { Entity } from '@backstage/catalog-model'; import { useApi, useRouteRef, configApiRef } from '@backstage/core-plugin-api'; import { - Button, + LinkButton, ItemCardGrid, ItemCardHeader, } from '@backstage/core-components'; @@ -58,7 +58,7 @@ export const DocsCardGrid = (props: DocsCardGridProps) => { </CardMedia> <CardContent>{entity.metadata.description}</CardContent> <CardActions> - <Button + <LinkButton to={getRouteToReaderPageFor({ namespace: toLowerMaybe( entity.metadata.namespace ?? 'default', @@ -71,7 +71,7 @@ export const DocsCardGrid = (props: DocsCardGridProps) => { data-testid="read_docs" > Read Docs - </Button> + </LinkButton> </CardActions> </Card> ))} diff --git a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx index 883c496191..5e5b41ee3e 100644 --- a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx @@ -25,10 +25,11 @@ import { } from '@backstage/plugin-catalog-react'; import { rootDocsRouteRef } from '../../../routes'; import { - Button, + LinkButton, EmptyState, Table, TableColumn, + TableOptions, TableProps, } from '@backstage/core-components'; import { actionFactories } from './actions'; @@ -47,6 +48,7 @@ export type DocsTableProps = { loading?: boolean | undefined; columns?: TableColumn<DocsTableRow>[]; actions?: TableProps<DocsTableRow>['actions']; + options?: TableOptions<DocsTableRow>; }; /** @@ -55,7 +57,7 @@ export type DocsTableProps = { * @public */ export const DocsTable = (props: DocsTableProps) => { - const { entities, title, loading, columns, actions } = props; + const { entities, title, loading, columns, actions, options } = props; const [, copyToClipboard] = useCopyToClipboard(); const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef); const config = useApi(configApiRef); @@ -102,6 +104,7 @@ export const DocsTable = (props: DocsTableProps) => { pageSize: 20, search: true, actionsColumnIndex: -1, + ...options, }} data={documents} columns={columns || defaultColumns} @@ -118,13 +121,13 @@ export const DocsTable = (props: DocsTableProps) => { title="No documents to show" description="Create your own document. Check out our Getting Started Information" action={ - <Button + <LinkButton color="primary" to="https://backstage.io/docs/features/techdocs/getting-started" variant="contained" > DOCS - </Button> + </LinkButton> } /> )} diff --git a/plugins/techdocs/src/home/components/Tables/EntityListDocsTable.tsx b/plugins/techdocs/src/home/components/Tables/EntityListDocsTable.tsx index d47ddb13c4..0042cc9418 100644 --- a/plugins/techdocs/src/home/components/Tables/EntityListDocsTable.tsx +++ b/plugins/techdocs/src/home/components/Tables/EntityListDocsTable.tsx @@ -20,6 +20,7 @@ import { capitalize } from 'lodash'; import { CodeSnippet, TableColumn, + TableOptions, TableProps, WarningPanel, } from '@backstage/core-components'; @@ -40,6 +41,7 @@ import { DocsTableRow } from './types'; export type EntityListDocsTableProps = { columns?: TableColumn<DocsTableRow>[]; actions?: TableProps<DocsTableRow>['actions']; + options?: TableOptions<DocsTableRow>; }; /** @@ -48,7 +50,7 @@ export type EntityListDocsTableProps = { * @public */ export const EntityListDocsTable = (props: EntityListDocsTableProps) => { - const { columns, actions } = props; + const { columns, actions, options } = props; const { loading, error, entities, filters } = useEntityList(); const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); const [, copyToClipboard] = useCopyToClipboard(); @@ -81,6 +83,7 @@ export const EntityListDocsTable = (props: EntityListDocsTableProps) => { loading={loading} actions={actions || defaultActions} columns={columns} + options={options} /> ); }; diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index 13b71d49ed..c759d43fe9 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -37,11 +37,14 @@ export { TechDocsIndexPage, TechdocsPage, TechDocsReaderPage, + TechDocsSearchResultListItem, techdocsPlugin as plugin, techdocsPlugin, } from './plugin'; export * from './Router'; +export type { TechDocsSearchResultListItemProps } from './search/components/TechDocsSearchResultListItem'; + /** * @deprecated Import from `@backstage/plugin-techdocs-react` instead * diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 4ad8b98e37..bdd4814898 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -33,6 +33,11 @@ import { fetchApiRef, identityApiRef, } from '@backstage/core-plugin-api'; +import { + createSearchResultListItemExtension, + SearchResultListItemExtensionProps, +} from '@backstage/plugin-search-react'; +import { TechDocsSearchResultListItemProps } from './search/components/TechDocsSearchResultListItem'; /** * The Backstage plugin that renders technical documentation for your components @@ -153,3 +158,21 @@ export const TechDocsReaderPage = techdocsPlugin.provide( mountPoint: rootDocsRouteRef, }), ); + +/** + * React extension used to render results on Search page or modal + * + * @public + */ +export const TechDocsSearchResultListItem: ( + props: SearchResultListItemExtensionProps<TechDocsSearchResultListItemProps>, +) => JSX.Element | null = techdocsPlugin.provide( + createSearchResultListItemExtension({ + name: 'TechDocsSearchResultListItem', + component: () => + import('./search/components/TechDocsSearchResultListItem').then( + m => m.TechDocsSearchResultListItem, + ), + predicate: result => result.type === 'techdocs', + }), +); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index ffe95e0b7e..ea7e4ed77b 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -102,6 +102,7 @@ CONFIGURATION 4: <TechDocsReaderPage> and provided content in <Route> <ExpandableNavigation /> <ReportIssue /> <TextSize /> + <LightBox /> </TechDocsAddons> </Route> diff --git a/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts b/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts index 9c4ee08346..cac7f76a0e 100644 --- a/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts +++ b/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts @@ -18,6 +18,7 @@ import { createTestShadowDom } from '../../test-utils'; import { copyToClipboard } from './copyToClipboard'; import { lightTheme } from '@backstage/theme'; import { waitFor } from '@testing-library/react'; +import useCopyToClipboard from 'react-use/lib/useCopyToClipboard'; const clipboardSpy = jest.fn(); Object.defineProperty(window.navigator, 'clipboard', { @@ -26,8 +27,21 @@ Object.defineProperty(window.navigator, 'clipboard', { }, }); +jest.mock('react-use/lib/useCopyToClipboard', () => { + const original = jest.requireActual('react-use/lib/useCopyToClipboard'); + + return { + __esModule: true, + default: jest.fn().mockImplementation(original.default), + }; +}); + describe('copyToClipboard', () => { it('calls navigator.clipboard.writeText when clipboard button has been clicked', async () => { + const spy = useCopyToClipboard as jest.Mock; + const copy = jest.fn(); + spy.mockReturnValue([{}, copy]); + const expectedClipboard = 'function foo() {return "bar";}'; const shadowDom = await createTestShadowDom( ` @@ -51,7 +65,7 @@ describe('copyToClipboard', () => { expect(tooltip).toHaveTextContent('Copied to clipboard'); }); - expect(clipboardSpy).toHaveBeenCalledWith(expectedClipboard); + expect(copy).toHaveBeenCalledWith(expectedClipboard); }); it('only gets applied to code blocks', async () => { diff --git a/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx b/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx index a90c4ca116..422bbb10ce 100644 --- a/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx +++ b/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx @@ -25,6 +25,7 @@ import { } from '@material-ui/core'; import Button from '@material-ui/core/Button'; import type { Transformer } from './transformer'; +import useCopyToClipboard from 'react-use/lib/useCopyToClipboard'; const CopyToClipboardTooltip = withStyles(theme => ({ tooltip: { @@ -49,11 +50,12 @@ type CopyToClipboardButtonProps = { const CopyToClipboardButton = ({ text }: CopyToClipboardButtonProps) => { const [open, setOpen] = useState(false); + const [, copyToClipboard] = useCopyToClipboard(); const handleClick = useCallback(() => { - window.navigator.clipboard.writeText(text); + copyToClipboard(text); setOpen(true); - }, [text]); + }, [text, copyToClipboard]); const handleClose = useCallback(() => { setOpen(false); diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index a197a2d4b7..61498a37b0 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -15,12 +15,12 @@ */ import DOMPurify from 'dompurify'; -import { useMemo, useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; -import { useApi, configApiRef } from '@backstage/core-plugin-api'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { Transformer } from '../transformer'; -import { removeUnsafeLinks, removeUnsafeIframes } from './hooks'; +import { removeUnsafeIframes, removeUnsafeLinks } from './hooks'; /** * Returns html sanitizer configuration @@ -34,7 +34,7 @@ const useSanitizerConfig = () => { }; /** - * Returns a transformer that sanitizes the dom's internal html. + * Returns a transformer that sanitizes the dom */ export const useSanitizerTransformer = (): Transformer => { const config = useSanitizerConfig(); @@ -51,7 +51,8 @@ export const useSanitizerTransformer = (): Transformer => { DOMPurify.addHook('beforeSanitizeElements', removeUnsafeIframes(hosts)); } - return DOMPurify.sanitize(dom.innerHTML, { + // using outerHTML as we want to preserve the html tag attributes (lang) + return DOMPurify.sanitize(dom.outerHTML, { ADD_TAGS: tags, FORBID_TAGS: ['style'], WHOLE_DOCUMENT: true, diff --git a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts index 896f909be4..e222c24a4c 100644 --- a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts +++ b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts @@ -15,46 +15,104 @@ */ import { scrollIntoAnchor } from '../transformers'; - -jest.useFakeTimers(); +import { createTestShadowDom } from '../../test-utils'; +import { Transformer } from './transformer'; +import mkdocsIndex from '../../test-utils/fixtures/mkdocs-index'; +import { SHADOW_DOM_STYLE_LOAD_EVENT } from '@backstage/plugin-techdocs-react'; describe('scrollIntoAnchor', () => { - const transformer = scrollIntoAnchor(); - const dom = { querySelector: jest.fn() }; + const scrollIntoView = jest.fn(); + let querySelectorSpy: jest.SpyInstance; + let addEventListenerSpy: jest.SpyInstance; + let removeEventListenerSpy: jest.SpyInstance; + const applySpies: Transformer = dom => { + querySelectorSpy = jest.spyOn(dom, 'querySelector'); + querySelectorSpy.mockReturnValue({ scrollIntoView }); + addEventListenerSpy = jest.spyOn(dom, 'addEventListener'); + removeEventListenerSpy = jest.spyOn(dom, 'removeEventListener'); + return dom; + }; afterEach(() => { jest.clearAllMocks(); }); it('does nothing if there is no anchor element', async () => { - transformer(dom as unknown as Element); - jest.advanceTimersByTime(200); - expect(dom.querySelector).not.toHaveBeenCalled(); + await createTestShadowDom(mkdocsIndex, { + preTransformers: [], + postTransformers: [applySpies, scrollIntoAnchor()], + }); + expect(querySelectorSpy).not.toHaveBeenCalled(); + expect(addEventListenerSpy).toHaveBeenCalled(); + expect(removeEventListenerSpy).toHaveBeenCalled(); + expect(addEventListenerSpy).toHaveBeenCalledWith( + SHADOW_DOM_STYLE_LOAD_EVENT, + expect.any(Function), + ); + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); + expect(removeEventListenerSpy).toHaveBeenCalledWith( + SHADOW_DOM_STYLE_LOAD_EVENT, + expect.any(Function), + ); + // check that the same function is passed to both addEventListener and removeEventListener + expect(addEventListenerSpy.mock.calls[0][1]).toBe( + removeEventListenerSpy.mock.calls[0][1], + ); }); it('scroll to the hash anchor element', async () => { - const scrollIntoView = jest.fn(); - dom.querySelector.mockReturnValue({ scrollIntoView }); window.location.hash = '#hash'; - transformer(dom as unknown as Element); - jest.advanceTimersByTime(200); - expect(dom.querySelector).toHaveBeenCalledWith( + await createTestShadowDom(mkdocsIndex, { + preTransformers: [], + postTransformers: [applySpies, scrollIntoAnchor()], + }); + expect(querySelectorSpy).toHaveBeenCalledWith( expect.stringMatching('[id="hash"]'), ); expect(scrollIntoView).toHaveBeenCalledWith(); + expect(addEventListenerSpy).toHaveBeenCalledTimes(1); + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); + expect(addEventListenerSpy).toHaveBeenCalledWith( + SHADOW_DOM_STYLE_LOAD_EVENT, + expect.any(Function), + ); + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); + expect(removeEventListenerSpy).toHaveBeenCalledWith( + SHADOW_DOM_STYLE_LOAD_EVENT, + expect.any(Function), + ); + // check that the same function is passed to both addEventListener and removeEventListener + expect(addEventListenerSpy.mock.calls[0][1]).toBe( + removeEventListenerSpy.mock.calls[0][1], + ); window.location.hash = ''; }); it('works for anchor starting with number', async () => { - const scrollIntoView = jest.fn(); - dom.querySelector.mockReturnValue({ scrollIntoView }); + querySelectorSpy.mockReturnValue({ scrollIntoView }); window.location.hash = '#1-hash'; - transformer(dom as unknown as Element); - jest.advanceTimersByTime(200); - expect(dom.querySelector).toHaveBeenCalledWith( + await createTestShadowDom(mkdocsIndex, { + preTransformers: [], + postTransformers: [applySpies, scrollIntoAnchor()], + }); + expect(querySelectorSpy).toHaveBeenCalledWith( expect.stringMatching('[id="1-hash"]'), ); expect(scrollIntoView).toHaveBeenCalledWith(); + expect(addEventListenerSpy).toHaveBeenCalledTimes(1); + expect(addEventListenerSpy).toHaveBeenCalledWith( + SHADOW_DOM_STYLE_LOAD_EVENT, + expect.any(Function), + ); + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); + expect(removeEventListenerSpy).toHaveBeenCalledWith( + SHADOW_DOM_STYLE_LOAD_EVENT, + expect.any(Function), + ); + // check that the same function is passed to both addEventListener and removeEventListener + expect(addEventListenerSpy.mock.calls[0][1]).toBe( + removeEventListenerSpy.mock.calls[0][1], + ); window.location.hash = ''; }); }); diff --git a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts index 7ba02bcdfc..97a18b7241 100644 --- a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts +++ b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts @@ -15,17 +15,23 @@ */ import type { Transformer } from './transformer'; +import { SHADOW_DOM_STYLE_LOAD_EVENT } from '@backstage/plugin-techdocs-react'; export const scrollIntoAnchor = (): Transformer => { return dom => { - setTimeout(() => { - // Scroll to the desired anchor on initial navigation - if (window.location.hash) { - const hash = window.location.hash.slice(1); - // fix invalid selector error for anchor starting with number - dom?.querySelector(`[id="${hash}"]`)?.scrollIntoView(); - } - }, 200); + dom.addEventListener( + SHADOW_DOM_STYLE_LOAD_EVENT, + function handleShadowDomStyleLoad() { + if (window.location.hash) { + const hash = window.location.hash.slice(1); + dom?.querySelector(`[id="${hash}"]`)?.scrollIntoView(); + } + dom.removeEventListener( + SHADOW_DOM_STYLE_LOAD_EVENT, + handleShadowDomStyleLoad, + ); + }, + ); return dom; }; }; diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts index c4e6938fdc..979a616f01 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts @@ -105,7 +105,13 @@ export default ({ theme, sidebar }: RuleOptions) => ` .md-footer { position: fixed; bottom: 0px; + pointer-events: none; } + +.md-footer-nav__link, .md-footer__link { + pointer-events: all; +} + .md-footer__title { background-color: unset; } diff --git a/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx index 800fba2260..3c0d2314e2 100644 --- a/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx @@ -15,16 +15,9 @@ */ import React, { PropsWithChildren, ReactNode } from 'react'; -import { - Divider, - ListItem, - ListItemIcon, - ListItemText, - makeStyles, -} from '@material-ui/core'; +import { ListItemIcon, ListItemText, makeStyles } from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; import { Link } from '@backstage/core-components'; -import { useAnalytics } from '@backstage/core-plugin-api'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; @@ -44,8 +37,8 @@ const useStyles = makeStyles({ * @public */ export type TechDocsSearchResultListItemProps = { - icon?: ReactNode; - result: any; + icon?: ReactNode | ((result: any) => ReactNode); + result?: any; highlight?: ResultHighlight; rank?: number; lineClamp?: number; @@ -65,7 +58,6 @@ export const TechDocsSearchResultListItem = ( const { result, highlight, - rank, lineClamp = 5, asListItem = true, asLink = true, @@ -74,17 +66,9 @@ export const TechDocsSearchResultListItem = ( } = props; const classes = useStyles(); - const analytics = useAnalytics(); - const handleClick = () => { - analytics.captureEvent('discover', result.title, { - attributes: { to: result.location }, - value: rank, - }); - }; - const LinkWrapper = ({ children }: PropsWithChildren<{}>) => asLink ? ( - <Link noTrack to={result.location} onClick={handleClick}> + <Link noTrack to={result.location}> {children} </Link> ) : ( @@ -122,6 +106,8 @@ export const TechDocsSearchResultListItem = ( result.name ); + if (!result) return null; + return ( <ListItemText className={classes.itemText} @@ -165,11 +151,12 @@ export const TechDocsSearchResultListItem = ( const ListItemWrapper = ({ children }: PropsWithChildren<{}>) => asListItem ? ( <> - <ListItem alignItems="flex-start"> - {icon && <ListItemIcon>{icon}</ListItemIcon>} - <div className={classes.flexContainer}>{children}</div> - </ListItem> - <Divider component="li" /> + {icon && ( + <ListItemIcon> + {typeof icon === 'function' ? icon(result) : icon} + </ListItemIcon> + )} + <div className={classes.flexContainer}>{children}</div> </> ) : ( <>{children}</> diff --git a/plugins/techdocs/src/search/components/index.ts b/plugins/techdocs/src/search/components/index.ts index 1f7615b4ff..c70681fc53 100644 --- a/plugins/techdocs/src/search/components/index.ts +++ b/plugins/techdocs/src/search/components/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export * from './TechDocsSearchResultListItem'; export * from './TechDocsSearch'; diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index 566dadaf2e..39a9a56a70 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-todo-backend +## 0.1.40-next.0 + +### Patch Changes + +- 4120513412: todo-backend is now exposed as a plugin which uses the new plugin system +- Updated dependencies + - @backstage/catalog-client@1.4.0-next.0 + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.3.4-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.39 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/catalog-client@1.3.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-client@1.3.1-next.0 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## 0.1.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/catalog-client@1.3.1-next.0 + ## 0.1.37 ### Patch Changes diff --git a/plugins/todo-backend/api-report.md b/plugins/todo-backend/api-report.md index 277c87fea9..6db1e012e9 100644 --- a/plugins/todo-backend/api-report.md +++ b/plugins/todo-backend/api-report.md @@ -3,12 +3,14 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; import { ScmIntegrations } from '@backstage/integration'; +import { ServiceRef } from '@backstage/backend-plugin-api'; import { UrlReader } from '@backstage/backend-common'; // @public (undocumented) @@ -88,6 +90,9 @@ export type TodoParserResult = { lineNumber: number; }; +// @public +export const todoPlugin: () => BackendFeature; + // @public (undocumented) export interface TodoReader { readTodos(options: ReadTodosOptions): Promise<ReadTodosResult>; @@ -113,6 +118,9 @@ export type TodoReaderServiceOptions = { defaultPageSize?: number; }; +// @public (undocumented) +export const todoReaderServiceRef: ServiceRef<TodoReader, 'plugin'>; + // @public (undocumented) export class TodoScmReader implements TodoReader { constructor(options: TodoScmReaderOptions); diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 9f0d58bc86..f95ae498a7 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo-backend", "description": "A Backstage backend plugin that lets you browse TODO comments in your source code", - "version": "0.1.37", + "version": "0.1.40-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,13 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -45,7 +47,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/todo-backend/src/index.ts b/plugins/todo-backend/src/index.ts index 973c91df44..b1654327d6 100644 --- a/plugins/todo-backend/src/index.ts +++ b/plugins/todo-backend/src/index.ts @@ -22,3 +22,4 @@ export * from './lib'; export * from './service'; +export * from './plugin'; diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 2696f0d54b..276d176efb 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { loggerToWinstonLogger, UrlReader } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { Logger } from 'winston'; @@ -28,6 +28,11 @@ import { import { Config } from '@backstage/config'; import { createTodoParser } from './createTodoParser'; import path from 'path'; +import { + coreServices, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; const excludedExtensions = [ '.png', @@ -167,3 +172,26 @@ export class TodoScmReader implements TodoReader { return { result: { items: todos }, etag: tree.etag }; } } + +/** + * @public + */ +export const todoReaderServiceRef = createServiceRef<TodoReader>({ + id: 'todo.todoReader', + defaultFactory: async service => + createServiceFactory({ + service, + deps: { + reader: coreServices.urlReader, + config: coreServices.config, + logger: coreServices.logger, + }, + factory: async ({ reader, config, logger }) => { + const winstonLogger = loggerToWinstonLogger(logger); + return TodoScmReader.fromConfig(config, { + logger: winstonLogger, + reader, + }); + }, + }), +}); diff --git a/plugins/todo-backend/src/lib/TodoReader/index.ts b/plugins/todo-backend/src/lib/TodoReader/index.ts index d0537861da..a325fcbe65 100644 --- a/plugins/todo-backend/src/lib/TodoReader/index.ts +++ b/plugins/todo-backend/src/lib/TodoReader/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { TodoScmReader } from './TodoScmReader'; +export { TodoScmReader, todoReaderServiceRef } from './TodoScmReader'; export type { TodoScmReaderOptions } from './TodoScmReader'; export { createTodoParser } from './createTodoParser'; export type { TodoParserOptions } from './createTodoParser'; diff --git a/plugins/todo-backend/src/lib/utils.ts b/plugins/todo-backend/src/lib/utils.ts new file mode 100644 index 0000000000..cf0c2e4ea6 --- /dev/null +++ b/plugins/todo-backend/src/lib/utils.ts @@ -0,0 +1,107 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; + +export const parseIntegerParam = ( + str: unknown, + ctx: string, +): number | undefined => { + if (str === undefined) { + return undefined; + } + if (typeof str !== 'string') { + throw new InputError(`invalid ${ctx}, must be a string`); + } + const parsed = parseInt(str, 10); + if (!Number.isInteger(parsed) || String(parsed) !== str) { + throw new InputError(`invalid ${ctx}, not an integer`); + } + return parsed; +}; + +export const parseOrderByParam = <T extends readonly string[]>( + str: unknown, + allowedFields: T, +): { field: T[number]; direction: 'asc' | 'desc' } | undefined => { + if (str === undefined) { + return undefined; + } + if (typeof str !== 'string') { + throw new InputError(`invalid orderBy query, must be a string`); + } + const [field, direction] = str.split('='); + if (!field) { + throw new InputError(`invalid orderBy query, field name is empty`); + } + if (direction !== 'asc' && direction !== 'desc') { + throw new InputError( + `invalid orderBy query, order direction must be 'asc' or 'desc'`, + ); + } + + if (field && !allowedFields.includes(field)) { + throw new InputError( + `invalid orderBy field, must be one of ${allowedFields.join(', ')}`, + ); + } + return { field, direction }; +}; + +export const parseFilterParam = <T extends readonly string[]>( + str: unknown, + allowedFields: T, +): { field: T[number]; value: string }[] | undefined => { + if (str === undefined) { + return undefined; + } + + const filters = new Array<{ field: T[number]; value: string }>(); + + const strs = [str].flat(); + for (const filterStr of strs) { + if (typeof filterStr !== 'string') { + throw new InputError( + `invalid filter query, must be a string or list of strings`, + ); + } + const splitIndex = filterStr.indexOf('='); + if (splitIndex <= 0) { + throw new InputError( + `invalid filter query, must separate field from value using '='`, + ); + } + + const field = filterStr.slice(0, splitIndex); + if (!allowedFields.includes(field)) { + throw new InputError( + `invalid filter field, must be one of ${allowedFields.join(', ')}`, + ); + } + + const value = filterStr.slice(splitIndex + 1); + if (!value) { + throw new InputError(`invalid filter query, value may not be empty`); + } + filters.push({ field, value }); + } + + return filters; +}; + +export const getBearerToken = (header?: string): string | undefined => { + return header?.match(/Bearer\s+(\S+)/i)?.[1]; +}; diff --git a/plugins/todo-backend/src/plugin.ts b/plugins/todo-backend/src/plugin.ts new file mode 100644 index 0000000000..b9a73a84ee --- /dev/null +++ b/plugins/todo-backend/src/plugin.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createBackendPlugin, + coreServices, +} from '@backstage/backend-plugin-api'; + +import { todoServiceRef } from './service/TodoReaderService'; +import { createRouter } from './service/router'; + +/** + * The Todos plugin is responsible for aggregating todo comments within source. + * @public + */ +export const todoPlugin = createBackendPlugin({ + pluginId: 'todo-backend', + register(env) { + env.registerInit({ + deps: { + todoReader: todoServiceRef, + http: coreServices.httpRouter, + }, + async init({ http, todoReader }) { + http.use( + await createRouter({ + todoService: todoReader, + }), + ); + }, + }); + }, +}); diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 35fd76b8be..6ce38709f1 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -20,7 +20,14 @@ import { getEntitySourceLocation, stringifyEntityRef, } from '@backstage/catalog-model'; -import { TodoReader } from '../lib'; +import { + createServiceFactory, + createServiceRef, + ServiceRef, +} from '@backstage/backend-plugin-api'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; + +import { TodoReader, todoReaderServiceRef } from '../lib'; import { ListTodosRequest, ListTodosResponse, TodoService } from './types'; const DEFAULT_DEFAULT_PAGE_SIZE = 10; @@ -130,3 +137,23 @@ export class TodoReaderService implements TodoService { }; } } + +export const todoServiceRef: ServiceRef<TodoService> = + createServiceRef<TodoService>({ + id: 'todo.client', + defaultFactory: async service => + createServiceFactory({ + service, + deps: { + catalogApi: catalogServiceRef, + todoReader: todoReaderServiceRef, + }, + async factory({ catalogApi, todoReader }) { + const todoReaderService = new TodoReaderService({ + catalogClient: catalogApi, + todoReader, + }); + return todoReaderService; + }, + }), + }); diff --git a/plugins/todo-backend/src/service/router.test.ts b/plugins/todo-backend/src/service/router.test.ts index 0254147681..f2d9f2c7a3 100644 --- a/plugins/todo-backend/src/service/router.test.ts +++ b/plugins/todo-backend/src/service/router.test.ts @@ -18,12 +18,12 @@ import express from 'express'; import request from 'supertest'; import { errorHandler } from '@backstage/backend-common'; +import { createRouter } from './router'; import { - createRouter, parseFilterParam, parseIntegerParam, parseOrderByParam, -} from './router'; +} from '../lib/utils'; import { TodoService } from './types'; const mockListBody = { diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index 33356a83d6..5954fab009 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -18,15 +18,13 @@ import { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model'; import { InputError } from '@backstage/errors'; import express from 'express'; import Router from 'express-promise-router'; -import { TodoService } from './types'; - -const TODO_FIELDS = [ - 'text', - 'tag', - 'author', - 'viewUrl', - 'repoFilePath', -] as const; +import { type TodoService, TODO_FIELDS } from './types'; +import { + getBearerToken, + parseFilterParam, + parseIntegerParam, + parseOrderByParam, +} from '../lib/utils'; /** @public */ export interface RouterOptions { @@ -78,93 +76,3 @@ export async function createRouter( return router; } - -export function parseIntegerParam( - str: unknown, - ctx: string, -): number | undefined { - if (str === undefined) { - return undefined; - } - if (typeof str !== 'string') { - throw new InputError(`invalid ${ctx}, must be a string`); - } - const parsed = parseInt(str, 10); - if (!Number.isInteger(parsed) || String(parsed) !== str) { - throw new InputError(`invalid ${ctx}, not an integer`); - } - return parsed; -} - -export function parseOrderByParam<T extends readonly string[]>( - str: unknown, - allowedFields: T, -): { field: T[number]; direction: 'asc' | 'desc' } | undefined { - if (str === undefined) { - return undefined; - } - if (typeof str !== 'string') { - throw new InputError(`invalid orderBy query, must be a string`); - } - const [field, direction] = str.split('='); - if (!field) { - throw new InputError(`invalid orderBy query, field name is empty`); - } - if (direction !== 'asc' && direction !== 'desc') { - throw new InputError( - `invalid orderBy query, order direction must be 'asc' or 'desc'`, - ); - } - - if (field && !allowedFields.includes(field)) { - throw new InputError( - `invalid orderBy field, must be one of ${allowedFields.join(', ')}`, - ); - } - return { field, direction }; -} - -export function parseFilterParam<T extends readonly string[]>( - str: unknown, - allowedFields: T, -): { field: T[number]; value: string }[] | undefined { - if (str === undefined) { - return undefined; - } - - const filters = new Array<{ field: T[number]; value: string }>(); - - const strs = [str].flat(); - for (const filterStr of strs) { - if (typeof filterStr !== 'string') { - throw new InputError( - `invalid filter query, must be a string or list of strings`, - ); - } - const splitIndex = filterStr.indexOf('='); - if (splitIndex <= 0) { - throw new InputError( - `invalid filter query, must separate field from value using '='`, - ); - } - - const field = filterStr.slice(0, splitIndex); - if (!allowedFields.includes(field)) { - throw new InputError( - `invalid filter field, must be one of ${allowedFields.join(', ')}`, - ); - } - - const value = filterStr.slice(splitIndex + 1); - if (!value) { - throw new InputError(`invalid filter query, value may not be empty`); - } - filters.push({ field, value }); - } - - return filters; -} - -function getBearerToken(header?: string): string | undefined { - return header?.match(/Bearer\s+(\S+)/i)?.[1]; -} diff --git a/plugins/todo-backend/src/service/types.ts b/plugins/todo-backend/src/service/types.ts index 88d2f2b340..ab15e20027 100644 --- a/plugins/todo-backend/src/service/types.ts +++ b/plugins/todo-backend/src/service/types.ts @@ -48,3 +48,11 @@ export interface TodoService { options?: { token?: string }, ): Promise<ListTodosResponse>; } + +export const TODO_FIELDS = [ + 'text', + 'tag', + 'author', + 'viewUrl', + 'repoFilePath', +] as const; diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index 1788011fcb..35c607ba30 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-todo +## 0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## 0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.2.16 ### Patch Changes diff --git a/plugins/todo/package.json b/plugins/todo/package.json index a1868ebe92..8c925c5cf7 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,14 +1,15 @@ { "name": "@backstage/plugin-todo", "description": "A Backstage plugin that lets you browse TODO comments in your source code", - "version": "0.2.16", + "version": "0.2.18-next.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" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "frontend-plugin" @@ -20,7 +21,7 @@ "directory": "plugins/todo" }, "scripts": { - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "start": "backstage-cli package start", "lint": "backstage-cli package lint", "test": "backstage-cli package test", @@ -54,9 +55,10 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 1eb35b3ba5..4a6c211adb 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-user-settings-backend +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.1-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.12-next.0 + +## 0.1.6 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0 + - @backstage/backend-common@0.18.2 + - @backstage/catalog-model@1.2.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11 + +## 0.1.6-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-auth-node@0.2.11-next.1 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/plugin-auth-node@0.2.11-next.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/user-settings-backend/api-report.md b/plugins/user-settings-backend/api-report.md index 1bc37bbec7..b015915723 100644 --- a/plugins/user-settings-backend/api-report.md +++ b/plugins/user-settings-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 { BackendFeature } from '@backstage/backend-plugin-api'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -18,5 +19,8 @@ export interface RouterOptions { identity: IdentityApi; } +// @alpha +export const userSettingsPlugin: () => BackendFeature; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index b64b30f19d..61ef65c231 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings-backend", "description": "The Backstage backend plugin to manage user settings", - "version": "0.1.4", + "version": "0.1.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,7 +11,8 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "repository": { "type": "git", @@ -20,7 +21,7 @@ }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -29,6 +30,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", @@ -48,6 +50,7 @@ }, "files": [ "dist", - "migrations" + "migrations", + "alpha" ] } diff --git a/plugins/user-settings-backend/src/index.ts b/plugins/user-settings-backend/src/index.ts index 04d8accdaf..5504aaa564 100644 --- a/plugins/user-settings-backend/src/index.ts +++ b/plugins/user-settings-backend/src/index.ts @@ -16,3 +16,4 @@ export * from './service'; export * from './database'; +export { userSettingsPlugin } from './plugin'; diff --git a/packages/backend-test-utils/src/next/implementations/mockTokenManagerService.ts b/plugins/user-settings-backend/src/plugin.ts similarity index 55% rename from packages/backend-test-utils/src/next/implementations/mockTokenManagerService.ts rename to plugins/user-settings-backend/src/plugin.ts index 2cfc15220e..e200d22ffe 100644 --- a/packages/backend-test-utils/src/next/implementations/mockTokenManagerService.ts +++ b/plugins/user-settings-backend/src/plugin.ts @@ -13,28 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TokenManager } from '@backstage/backend-common'; + import { + createBackendPlugin, coreServices, - createServiceFactory, } from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; -class TokenManagerMock implements TokenManager { - async getToken(): Promise<{ token: string }> { - return { token: 'mock-token' }; - } - async authenticate(token: string): Promise<void> { - if (token !== 'mock-token') { - throw new Error('Invalid token'); - } - } -} - -/** @alpha */ -export const mockTokenManagerFactory = createServiceFactory({ - service: coreServices.tokenManager, - deps: {}, - async factory() { - return new TokenManagerMock(); +/** + * The user settings backend plugin. + * + * @alpha + */ +export const userSettingsPlugin = createBackendPlugin({ + pluginId: 'userSettings', + register(env) { + env.registerInit({ + deps: { + database: coreServices.database, + identity: coreServices.identity, + httpRouter: coreServices.httpRouter, + }, + async init({ database, identity, httpRouter }) { + httpRouter.use(await createRouter({ database, identity })); + }, + }); }, }); diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 7d156138db..784956b16d 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-user-settings +## 0.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-app-api@1.5.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + - @backstage/types@1.0.2 + +## 0.7.0 + +### Minor Changes + +- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults + +### Patch Changes + +- c4940b6322: Added a message to advise a page reload when toggling feature flags +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-app-api@1.5.0 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.6.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## 0.6.3-next.0 + +### Patch Changes + +- c4940b6322: Added a message to advise a page reload when toggling feature flags +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + ## 0.6.2 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 16c120fb35..cce6dfa1e5 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.6.2", + "version": "0.7.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -59,7 +59,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx index f8e1fb190c..772ec80982 100644 --- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -24,6 +24,7 @@ import { oktaAuthApiRef, microsoftAuthApiRef, bitbucketAuthApiRef, + bitbucketServerAuthApiRef, atlassianAuthApiRef, oneloginAuthApiRef, } from '@backstage/core-plugin-api'; @@ -99,6 +100,14 @@ export const DefaultProviderSettings = (props: { icon={Star} /> )} + {configuredProviders.includes('bitbucketServer') && ( + <ProviderSettingsItem + title="Bitbucket Server" + description="Provides authentication towards Bitbucket Server APIs" + apiRef={bitbucketServerAuthApiRef} + icon={Star} + /> + )} </> ); }; diff --git a/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx index a3b5f593ae..c58e637bd2 100644 --- a/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx @@ -82,6 +82,9 @@ export const UserSettingsFeatureFlags = () => { <Grid container style={{ justifyContent: 'space-between' }}> <Grid item xs={6} md={8}> <Typography variant="h5">Feature Flags</Typography> + <Typography variant="subtitle1"> + Please refresh the page when toggling feature flags + </Typography> </Grid> {featureFlags.length >= 10 && ( <Grid item xs={6} md={4}> diff --git a/plugins/vault-backend/CHANGELOG.md b/plugins/vault-backend/CHANGELOG.md index 1d7090b880..4f57faf78e 100644 --- a/plugins/vault-backend/CHANGELOG.md +++ b/plugins/vault-backend/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-vault-backend +## 0.2.10-next.0 + +### Patch Changes + +- 66b3a3956b: Ignore the `eslint` error +- Updated dependencies + - @backstage/backend-tasks@0.4.4-next.0 + - @backstage/backend-common@0.18.3-next.0 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.8 + +### Patch Changes + +- 5b7cd5580d: Moving the backend-test-utils to devDependencies. +- Updated dependencies + - @backstage/backend-common@0.18.2 + - @backstage/backend-tasks@0.4.3 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.1 + - @backstage/backend-tasks@0.4.3-next.1 + - @backstage/backend-test-utils@0.1.34-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## 0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.0 + - @backstage/backend-common@0.18.2-next.0 + - @backstage/backend-tasks@0.4.3-next.0 + ## 0.2.6 ### Patch Changes diff --git a/plugins/vault-backend/package.json b/plugins/vault-backend/package.json index 7d331a79ab..fda7059dc5 100644 --- a/plugins/vault-backend/package.json +++ b/plugins/vault-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-vault-backend", "description": "A Backstage backend plugin that integrates towards Vault", - "version": "0.2.6", + "version": "0.2.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,6 @@ "dependencies": { "@backstage/backend-common": "workspace:^", "@backstage/backend-tasks": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@types/express": "*", @@ -50,10 +49,11 @@ "yn": "^5.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/compression": "^1.7.2", "@types/supertest": "^2.0.8", - "msw": "^0.49.0", + "msw": "^1.0.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/vault-backend/src/service/standaloneApplication.ts b/plugins/vault-backend/src/service/standaloneApplication.ts index bcec8ca419..ba4c0edc3e 100644 --- a/plugins/vault-backend/src/service/standaloneApplication.ts +++ b/plugins/vault-backend/src/service/standaloneApplication.ts @@ -28,6 +28,10 @@ import helmet from 'helmet'; import { Logger } from 'winston'; import { createRouter } from './router'; import { ConfigReader } from '@backstage/config'; + +// Think this file will probably go away once we move to backend system +// And restructure into the /dev folder +// eslint-disable-next-line @backstage/no-undeclared-imports import { TestDatabases } from '@backstage/backend-test-utils'; import { TaskScheduler } from '@backstage/backend-tasks'; diff --git a/plugins/vault/CHANGELOG.md b/plugins/vault/CHANGELOG.md index b322ea9763..3b79e99c42 100644 --- a/plugins/vault/CHANGELOG.md +++ b/plugins/vault/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-vault +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.4.0-next.0 + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/catalog-model@1.2.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.1.9 + +### Patch Changes + +- fd7a77b9c4: Surface additional context and details to the Backstage UI when the Vault plugin encounters non-successful HTTP responses from the Vault API. +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/catalog-model@1.2.0 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/plugin-catalog-react@1.3.0 + - @backstage/errors@1.1.4 + +## 0.1.9-next.2 + +### Patch Changes + +- fd7a77b9c4: Surface additional context and details to the Backstage UI when the Vault plugin encounters non-successful HTTP responses from the Vault API. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/plugin-catalog-react@1.3.0-next.1 + - @backstage/catalog-model@1.1.6-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.3.0-next.0 + - @backstage/catalog-model@1.1.6-next.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/vault/package.json b/plugins/vault/package.json index ecece0283e..e0754b9c8d 100644 --- a/plugins/vault/package.json +++ b/plugins/vault/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-vault", "description": "A Backstage plugin that integrates towards Vault", - "version": "0.1.8", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -57,7 +57,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/vault/src/api.test.ts b/plugins/vault/src/api.test.ts index 4ce6f95c41..471cadc87e 100644 --- a/plugins/vault/src/api.test.ts +++ b/plugins/vault/src/api.test.ts @@ -50,8 +50,10 @@ describe('api', () => { const { path } = req.params; if (path === 'test/success') { return res(ctx.json(mockSecretsResult)); - } else if (path === 'test/error') { + } else if (path === 'test/empty') { return res(ctx.json({ items: [] })); + } else if (path === 'test/not-found') { + return res(ctx.status(404)); } return res(ctx.status(400)); }), @@ -72,7 +74,7 @@ describe('api', () => { it('should return empty secret list', async () => { setupHandlers(); const api = new VaultClient({ discoveryApi }); - expect(await api.listSecrets('test/error')).toEqual([]); + expect(await api.listSecrets('test/empty')).toEqual([]); }); it('should return all the secrets if no path defined', async () => { @@ -80,4 +82,20 @@ describe('api', () => { const api = new VaultClient({ discoveryApi }); expect(await api.listSecrets('')).toEqual(mockSecretsResult.items); }); + + it('should throw an error if the Vault API responds with an HTTP 404', async () => { + setupHandlers(); + const api = new VaultClient({ discoveryApi }); + await expect(api.listSecrets('test/not-found')).rejects.toThrow( + "No secrets found in path 'v1/secrets/test%2Fnot-found'", + ); + }); + + it('should throw an error if the Vault API responds with a non-successful HTTP status code', async () => { + setupHandlers(); + const api = new VaultClient({ discoveryApi }); + await expect(api.listSecrets('test/error')).rejects.toThrow( + 'Request failed with 400 Error', + ); + }); }); diff --git a/plugins/vault/src/api.ts b/plugins/vault/src/api.ts index bdd881e08c..84843c9176 100644 --- a/plugins/vault/src/api.ts +++ b/plugins/vault/src/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { DiscoveryApi, createApiRef } from '@backstage/core-plugin-api'; -import { NotFoundError } from '@backstage/errors'; +import { NotFoundError, ResponseError } from '@backstage/errors'; /** * @public @@ -75,9 +75,7 @@ export class VaultClient implements VaultApi { } else if (response.status === 404) { throw new NotFoundError(`No secrets found in path '${path}'`); } - throw new Error( - `Unexpected error while fetching secrets from path '${path}'`, - ); + throw await ResponseError.fromResponse(response); } async listSecrets(secretPath: string): Promise<VaultSecret[]> { diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx index 82a90f23e1..27cc0ed91e 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx @@ -33,40 +33,28 @@ describe('EntityVaultTable', () => { let apis: TestApiRegistry; const mockBaseUrl = 'https://api-vault.com/api/vault'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - - const entityOk: ComponentEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'test', - description: 'This is the description', - annotations: { - 'vault.io/secrets-path': 'test/success', + const entity = (secretPath: string) => { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + description: 'This is the description', + annotations: { + 'vault.io/secrets-path': secretPath, + }, }, - }, - spec: { - lifecycle: 'production', - owner: 'owner', - type: 'service', - }, + spec: { + lifecycle: 'production', + owner: 'owner', + type: 'service', + }, + } as ComponentEntity; }; - const entityNotOk: ComponentEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'test', - description: 'This is the description', - annotations: { - 'vault.io/secrets-path': 'test/error', - }, - }, - spec: { - lifecycle: 'production', - owner: 'owner', - type: 'service', - }, - }; + const entityOk = entity('test/success'); + const entityEmpty = entity('test/empty'); + const entityNotOk = entity('test/error'); const mockSecretsResult: { items: VaultSecret[] } = { items: [ @@ -91,7 +79,7 @@ describe('EntityVaultTable', () => { const { path } = req.params; if (path === 'test/success') { return res(ctx.json(mockSecretsResult)); - } else if (path === 'test/error') { + } else if (path === 'test/empty') { return res(ctx.json([])); } return res(ctx.status(400)); @@ -122,10 +110,25 @@ describe('EntityVaultTable', () => { setupHandlers(); const rendered = await renderInTestApp( <ApiProvider apis={apis}> - <EntityVaultTable entity={entityNotOk} /> + <EntityVaultTable entity={entityEmpty} /> </ApiProvider>, ); expect(rendered.getByText(/No secrets found/)).toBeInTheDocument(); }); + + it('should surface an appropriate error when the Vault API responds unsuccessfully', async () => { + setupHandlers(); + const rendered = await renderInTestApp( + <ApiProvider apis={apis}> + <EntityVaultTable entity={entityNotOk} /> + </ApiProvider>, + ); + + expect( + rendered.getByText( + /Unexpected error while fetching secrets from path \'test\/error\'\: Request failed with 400 Error/, + ), + ).toBeInTheDocument(); + }); }); diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx index efc345b07f..9191d54b0b 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx @@ -81,7 +81,12 @@ export const EntityVaultTable = ({ entity }: { entity: Entity }) => { }); if (error) { - return <Alert severity="error">{error.message}</Alert>; + return ( + <Alert severity="error"> + Unexpected error while fetching secrets from path '{secretPath}':{' '} + {error.message} + </Alert> + ); } return ( diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index 4d80489e99..7a727aa92a 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-xcmetrics +## 0.2.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.4.1-next.0 + - @backstage/core-components@0.12.5-next.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.17 + +## 0.2.35 + +### Patch Changes + +- c51efce2a0: Update docs to always use `yarn add --cwd` for app & backend +- Updated dependencies + - @backstage/core-components@0.12.4 + - @backstage/theme@0.2.17 + - @backstage/core-plugin-api@1.4.0 + - @backstage/errors@1.1.4 + +## 0.2.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## 0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + ## 0.2.34 ### Patch Changes diff --git a/plugins/xcmetrics/README.md b/plugins/xcmetrics/README.md index a3bb93d2af..7c003722a0 100644 --- a/plugins/xcmetrics/README.md +++ b/plugins/xcmetrics/README.md @@ -7,10 +7,9 @@ With this plugin, you can view data from XCMetrics directly in Backstage. ## Getting started -From `packages/app`: - ```bash -yarn add @backstage/plugin-xcmetrics +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-xcmetrics ``` In `packages/app/src/App.tsx`, add the following: diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index f4144693ae..107cb9600e 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-xcmetrics", "description": "A Backstage plugin that shows XCode build metrics for your components", - "version": "0.2.34", + "version": "0.2.36-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -49,7 +49,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.49.0" + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/scripts/assemble-manifest.js b/scripts/assemble-manifest.js index f68a83447d..f01bc7fd9b 100755 --- a/scripts/assemble-manifest.js +++ b/scripts/assemble-manifest.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2022 The Backstage Authors * diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 10c4b99976..2890f1cf33 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -21,7 +21,7 @@ const IGNORED = [ /^ADOPTERS\.md$/, /^OWNERS\.md$/, /^.*[/\\]CHANGELOG\.md$/, - /^.*[/\\]api-report\.md$/, + /^.*[/\\]([^\/]+-)?api-report\.md$/, /^docs[/\\]releases[/\\].*-changelog\.md$/, /^docs[/\\]reference[/\\]/, ]; @@ -56,7 +56,7 @@ async function listFiles(dir = '') { // caused by the script. In CI, we want to ensure vale linter is run. async function exitIfMissingVale() { try { - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-undeclared-imports await require('command-exists')('vale'); } catch (e) { if (process.env.CI) { diff --git a/scripts/check-if-release.js b/scripts/check-if-release.js index e7dabe14a4..c12b27b0ef 100755 --- a/scripts/check-if-release.js +++ b/scripts/check-if-release.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2020 The Backstage Authors * diff --git a/scripts/create-github-release.js b/scripts/create-github-release.js index 4b3891c5ce..b64d7a9cb5 100755 --- a/scripts/create-github-release.js +++ b/scripts/create-github-release.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2020 The Backstage Authors * diff --git a/scripts/create-release-tag.js b/scripts/create-release-tag.js index 28417024fd..311b46d4e5 100755 --- a/scripts/create-release-tag.js +++ b/scripts/create-release-tag.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2020 The Backstage Authors * diff --git a/scripts/generate-changeset-feedback.js b/scripts/generate-changeset-feedback.js deleted file mode 100644 index b9ad09ca72..0000000000 --- a/scripts/generate-changeset-feedback.js +++ /dev/null @@ -1,314 +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. - */ - -const fs = require('fs'); -const { execFile: execFileCb } = require('child_process'); -const { promisify } = require('util'); -const { - basename, - resolve: resolvePath, - relative: relativePath, -} = require('path'); - -const execFile = promisify(execFileCb); - -// Tells whether a path relative to the package directory has an effect -// on the published package -function isPublishedPath(path) { - if (path.startsWith('dev/')) { - return false; - } - if (path.includes('__mocks__')) { - return false; - } - if (path.includes('__fixtures__')) { - return false; - } - // Don't count manual modifications to the changelog - if (path === 'CHANGELOG.md') { - return false; - } - // API report changes by themselves don't count - if (path === 'api-report.md' || path === 'cli-report.md') { - return false; - } - // Lint changes don't count - if (path === '.eslintrc.js') { - return false; - } - - const name = basename(path); - if (name.startsWith('setupTests.')) { - return false; - } - if (name.includes('.test.')) { - return false; - } - if (name.includes('.stories.')) { - return false; - } - return true; -} - -async function listChangedFiles(ref) { - if (!ref) { - throw new Error('ref is required'); - } - - const { stdout } = await execFile('git', ['diff', '--name-only', ref]); - return stdout.trim().split(/\r?\n/); -} - -async function listPackages() { - const rootPkg = require(resolvePath(process.cwd(), './package.json')); - if (!rootPkg?.workspaces?.packages) { - throw new Error('No workspaces found in root package.json'); - } - - const pkgs = []; - - // Naive workspace lookup implementation, we can't shell out to yarn here as the implementation embedded in the repo - for (const pkgPath of rootPkg?.workspaces?.packages) { - const readDirRecursive = (dir, parts) => { - const [nextPart] = parts; - - // We've reached the end of the path pattern, check if package.json exists - if (!nextPart) { - try { - const pkg = require(resolvePath(dir, 'package.json')); - pkgs.push({ - path: relativePath(process.cwd(), dir), - name: pkg.name, - }); - } catch { - process.stderr.write(`Failed to read package.json in ${dir}\n`); - } - return; - } - - for (const filePath of fs.readdirSync(dir)) { - if (fs.statSync(resolvePath(dir, filePath)).isDirectory()) { - if (filePath === nextPart || nextPart === '*') { - readDirRecursive(resolvePath(dir, filePath), parts.slice(1)); - } - } - } - }; - - // Split the workspace paths by / and check each directory level recursively - readDirRecursive(process.cwd(), pkgPath.split('/')); - } - - return pkgs; -} - -async function loadChangesets(filePaths) { - const changesets = []; - for (const filePath of filePaths) { - if (!filePath.startsWith('.changeset/') || !filePath.endsWith('.md')) { - continue; - } - try { - const content = await fs.promises.readFile(filePath, 'utf8'); - let lines = content.split(/\r?\n/); - - lines = lines.slice(lines.findIndex(line => line === '---') + 1); - lines = lines.slice( - 0, - lines.findIndex(line => line === '---'), - ); - - const bumps = new Map(); - bumps.toJSON = () => Object.fromEntries(bumps); - for (const line of lines) { - const match = line.match(/^'(.*)': (patch|minor|major)$/); - if (!match) { - throw new Error(`Invalid changeset line: ${line}`); - } - - bumps.set(match[1], match[2]); - } - - changesets.push({ - filePath, - bumps, - }); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } - } - - return changesets; -} - -async function listChangedPackages(changedFiles, packages) { - const changedPackageMap = new Map(); - for (const filePath of changedFiles) { - for (const pkg of packages) { - if (filePath.startsWith(`${pkg.path}/`)) { - const pkgPath = relativePath(pkg.path, filePath); - if (!isPublishedPath(pkgPath)) { - break; - } - const entry = changedPackageMap.get(pkg.name); - if (entry) { - entry.files.push(pkgPath); - } else { - const pkgJson = require(resolvePath(pkg.path, 'package.json')); - - changedPackageMap.set(pkg.name, { - ...pkg, - version: pkgJson.version, - isStable: !pkgJson.version.startsWith('0.'), - isPrivate: Boolean(pkgJson.private), - files: [pkgPath], - }); - } - break; - } - } - } - return Array.from(changedPackageMap.values()); -} - -function formatSection(prefix = [], generator, suffix = ['']) { - const lines = Array.from(generator()); - if (lines.length === 0) { - return ''; - } - - return [...[prefix].flat(), ...lines, ...[suffix].flat(), '', ''].join('\n'); -} - -function formatSummary(changedPackages, changesets) { - const changedNames = new Set(changedPackages.map(pkg => pkg.name)); - - let output = ''; - - output += formatSection( - `## Missing Changesets - -The following package(s) are changed by this PR but do not have a changeset: -`, - function* section() { - for (const pkg of changedPackages) { - if (changesets.some(c => c.bumps.get(pkg.name))) { - continue; - } - if (pkg.isPrivate) { - continue; - } - yield `- **${pkg.name}**`; - } - }, - ` -See [CONTRIBUTING.md](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md#creating-changesets) for more information about how to add changesets. -`, - ); - - output += formatSection( - `## Unexpected Changesets - -The following changeset(s) reference packages that have not been changed in this PR: -`, - function* section() { - for (const c of changesets) { - const missing = Array.from(c.bumps.keys()).filter( - b => !changedNames.has(b), - ); - if (missing.length > 0) { - yield `- **${c.filePath}**: ${missing.join(', ')}`; - } - } - }, - ` -Note that only changes that affect the published package require changesets, for example changes to tests and storybook stories do not require changesets. -`, - ); - - output += formatSection( - `## Unnecessary Changesets - -The following package(s) are private and do not need a changeset: -`, - function* section() { - for (const pkg of changedPackages) { - if (changesets.some(c => c.bumps.get(pkg.name)) && pkg.isPrivate) { - yield `- **${pkg.name}**`; - } - } - }, - ); - - output += formatSection( - `## Changed Packages - -| Package Name | Package Path | Changeset Bump | Current Version | -|:-------------|:-------------|:--------------:|:----------------|`, - function* section() { - const bumpMap = { - undefined: -1, - patch: 0, - minor: 1, - major: 2, - }; - - for (const pkg of changedPackages) { - const maxBump = - changesets - .map(c => c.bumps.get(pkg.name)) - .reduce( - (max, bump) => (bumpMap[bump] > bumpMap[max] ? bump : max), - undefined, - ) ?? 'none'; - yield `| ${pkg.name} | ${pkg.path} | **${maxBump}** | \`v${pkg.version}\` |`; - } - }, - ); - - return output; -} - -async function main() { - const [diffRef = 'origin/master'] = process.argv.slice(2); - const changedFiles = await listChangedFiles(diffRef); - const packages = await listPackages(); - - const changesets = await loadChangesets(changedFiles); - const changedPackages = await listChangedPackages(changedFiles, packages); - - process.stderr.write( - JSON.stringify( - { - changesets, - changedPackages, - }, - null, - 2, - ), - ); - process.stderr.write('\n'); - - const summary = formatSummary(changedPackages, changesets); - process.stdout.write(summary); -} - -main().catch(error => { - console.error(error.stack); - process.exit(1); -}); diff --git a/scripts/list-deprecations.js b/scripts/list-deprecations.js index 8047158138..22aff96d67 100755 --- a/scripts/list-deprecations.js +++ b/scripts/list-deprecations.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const _ = require('lodash'); const fs = require('fs-extra'); diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index 973f4fad58..4019a71cf3 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2022 The Backstage Authors * diff --git a/scripts/snyk-github-issue-sync.ts b/scripts/snyk-github-issue-sync.ts index da64e2ea82..ea7d42a2a4 100755 --- a/scripts/snyk-github-issue-sync.ts +++ b/scripts/snyk-github-issue-sync.ts @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ import { Octokit } from '@octokit/rest'; import minimist from 'minimist'; // Generated by GitHub workflow .github/workflows/snyk-github-issue-creator diff --git a/scripts/verify-api-reference.js b/scripts/verify-api-reference.js index 84a526604d..091e803c33 100755 --- a/scripts/verify-api-reference.js +++ b/scripts/verify-api-reference.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { resolve: resolvePath } = require('path'); const { promises: fs } = require('fs'); diff --git a/scripts/verify-changesets.js b/scripts/verify-changesets.js index 9e3064eeac..ae3f07840a 100755 --- a/scripts/verify-changesets.js +++ b/scripts/verify-changesets.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { resolve: resolvePath } = require('path'); const fs = require('fs-extra'); diff --git a/scripts/verify-links.js b/scripts/verify-links.js index 05ad387c37..7664cadad5 100755 --- a/scripts/verify-links.js +++ b/scripts/verify-links.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { resolve: resolvePath, join: joinPath, dirname } = require('path'); const fs = require('fs').promises; diff --git a/scripts/verify-lockfile-duplicates.js b/scripts/verify-lockfile-duplicates.js index cc22664219..820fba2ea5 100644 --- a/scripts/verify-lockfile-duplicates.js +++ b/scripts/verify-lockfile-duplicates.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { execFile: execFileCb } = require('child_process'); const { resolve: resolvePath, dirname: dirnamePath } = require('path'); diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 94167a34a7..5584e16296 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -1801,17 +1801,17 @@ __metadata: linkType: hard "@storybook/addon-a11y@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/addon-a11y@npm:6.5.15" + version: 6.5.16 + resolution: "@storybook/addon-a11y@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/api": 6.5.15 - "@storybook/channels": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/components": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/theming": 6.5.15 + "@storybook/theming": 6.5.16 axe-core: ^4.2.0 core-js: ^3.8.2 global: ^4.4.0 @@ -1828,21 +1828,21 @@ __metadata: optional: true react-dom: optional: true - checksum: 409e1393f655099b199dd3ba279d926b42d11ca8ce37255eec1e5d6ed3526cae559cd02d3485b70023846c3430b41630c2b6e14ed288580fcce00a12c34f7fcb + checksum: 05ce7f696254782b521a5e946f7d58b207d854e69ce8b624a14de0192ce558f640b9dae821e122911059a24d2f9907e783e8dce5fb9288d5cfbdf0833aaab503 languageName: node linkType: hard "@storybook/addon-actions@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/addon-actions@npm:6.5.15" + version: 6.5.16 + resolution: "@storybook/addon-actions@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/api": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/components": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/theming": 6.5.15 + "@storybook/theming": 6.5.16 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -1863,23 +1863,23 @@ __metadata: optional: true react-dom: optional: true - checksum: ce0ca5fddeb196adec7757dd287085c19d89d26506539d07445df3759eca9817a779f2923cc5fd7dfc9f515788628f6c0f604b10366659ca46562fab980c64f5 + checksum: d506a932f38412fc234cd58b5f2c8a0bfb8f3820b0ce8042234e9bf4bd277a2befc2d8458d061405ee72722206756375f471a22c37ea32f384259fcbb1a2b6a5 languageName: node linkType: hard "@storybook/addon-controls@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/addon-controls@npm:6.5.15" + version: 6.5.16 + resolution: "@storybook/addon-controls@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/api": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/components": 6.5.15 - "@storybook/core-common": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-common": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/node-logger": 6.5.15 - "@storybook/store": 6.5.15 - "@storybook/theming": 6.5.15 + "@storybook/node-logger": 6.5.16 + "@storybook/store": 6.5.16 + "@storybook/theming": 6.5.16 core-js: ^3.8.2 lodash: ^4.17.21 ts-dedent: ^2.0.0 @@ -1891,19 +1891,19 @@ __metadata: optional: true react-dom: optional: true - checksum: c87b01e035f7e0e6f3a1e864333c03e44f76ccd04a687cbe19cde59cfa1109eb2b124c288e1cf68c3a291dc6c28c056852aa2413c8157b92792ef339a33142ef + checksum: a9f1f577e5d991ae271c9823662adf65952554303094a2e0127bfe9d48e2415796628dadc3cfbc767600e21588336bfd9cb43da59fe76507b2186f6a61da34b8 languageName: node linkType: hard "@storybook/addon-links@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/addon-links@npm:6.5.15" + version: 6.5.16 + resolution: "@storybook/addon-links@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.15 + "@storybook/router": 6.5.16 "@types/qs": ^6.9.5 core-js: ^3.8.2 global: ^4.4.0 @@ -1919,21 +1919,21 @@ __metadata: optional: true react-dom: optional: true - checksum: ef40b02a3f48de2f591486fb04910e996bcb8d5fd406e2d6b81752659551b366ffc64f6cfdb461585e52c0ae98fa102be8595678e63a27171f9e2a0e20869bd6 + checksum: 40fa5fcd98df3be50b3587efda79ddf0156eb0078dd0afec43e81e961475bc8583feec1314baabe59fe2dc8e5b9b4bb4a738435172c208f828d1538cd59882fe languageName: node linkType: hard "@storybook/addon-storysource@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/addon-storysource@npm:6.5.15" + version: 6.5.16 + resolution: "@storybook/addon-storysource@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/api": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/components": 6.5.15 - "@storybook/router": 6.5.15 - "@storybook/source-loader": 6.5.15 - "@storybook/theming": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/router": 6.5.16 + "@storybook/source-loader": 6.5.16 + "@storybook/theming": 6.5.16 core-js: ^3.8.2 estraverse: ^5.2.0 loader-utils: ^2.0.4 @@ -1948,7 +1948,7 @@ __metadata: optional: true react-dom: optional: true - checksum: e501c78ad8224b07a15fd8626399ebc63f131feb615ad6c6a3d6a7eac320b8fd01ec74d7406e8422c224771dfca17cfa159816b272e9f635e07dd67a8b4f8cb8 + checksum: 0e614542c99a0470c8b70881f2f5bd41f0d0f6e94c99b4efc2be9ff757a8021d00a4adc9e80c0c85dbdb3fe89f244fdf5633b0f0017fe565f81a7bde42dc96d2 languageName: node linkType: hard @@ -1974,17 +1974,17 @@ __metadata: languageName: node linkType: hard -"@storybook/addons@npm:6.5.15, @storybook/addons@npm:^6.0.0, @storybook/addons@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/addons@npm:6.5.15" +"@storybook/addons@npm:6.5.16, @storybook/addons@npm:^6.0.0, @storybook/addons@npm:^6.5.9": + version: 6.5.16 + resolution: "@storybook/addons@npm:6.5.16" dependencies: - "@storybook/api": 6.5.15 - "@storybook/channels": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/api": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.15 - "@storybook/theming": 6.5.15 + "@storybook/router": 6.5.16 + "@storybook/theming": 6.5.16 "@types/webpack-env": ^1.16.0 core-js: ^3.8.2 global: ^4.4.0 @@ -1992,7 +1992,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 9de4cb9ff51cde37c456a0315f47c751daf4412a8d90321b5249a9b05f48a44dc48f01bbf83c9f369f63232da423d58ed47e4c03d50d16a8835d372022095b70 + checksum: 0463150e4cf7bd2b2aaafdbaadfb4420e4e0a31eb651cfc1a2d7f4b4974caf67878712602474585dfa18f583000608598045594909959d2e9e2ec32ba004392d languageName: node linkType: hard @@ -2024,17 +2024,17 @@ __metadata: languageName: node linkType: hard -"@storybook/api@npm:6.5.15, @storybook/api@npm:^6.0.0": - version: 6.5.15 - resolution: "@storybook/api@npm:6.5.15" +"@storybook/api@npm:6.5.16, @storybook/api@npm:^6.0.0": + version: 6.5.16 + resolution: "@storybook/api@npm:6.5.16" dependencies: - "@storybook/channels": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.15 + "@storybook/router": 6.5.16 "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.5.15 + "@storybook/theming": 6.5.16 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -2048,31 +2048,31 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: cebf1b70309c9c4a135c4ad8d3ebd85d01cfa4942e43231831e67514604199d3ed26395bbe0f89954718498a800085bd7d6eaef61c5d702e3a669532a227bd93 + checksum: c873189ac1e501825d647903baa125899c492cee962cb86ebb7455110bd09194eeb6943f5c58a1f808ce4ee2e20e305f5604a4e60b07003c82a6fc6ceaee5ea9 languageName: node linkType: hard -"@storybook/builder-webpack4@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/builder-webpack4@npm:6.5.15" +"@storybook/builder-webpack4@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/builder-webpack4@npm:6.5.16" dependencies: "@babel/core": ^7.12.10 - "@storybook/addons": 6.5.15 - "@storybook/api": 6.5.15 - "@storybook/channel-postmessage": 6.5.15 - "@storybook/channels": 6.5.15 - "@storybook/client-api": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/components": 6.5.15 - "@storybook/core-common": 6.5.15 - "@storybook/core-events": 6.5.15 - "@storybook/node-logger": 6.5.15 - "@storybook/preview-web": 6.5.15 - "@storybook/router": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/channel-postmessage": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/node-logger": 6.5.16 + "@storybook/preview-web": 6.5.16 + "@storybook/router": 6.5.16 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.15 - "@storybook/theming": 6.5.15 - "@storybook/ui": 6.5.15 + "@storybook/store": 6.5.16 + "@storybook/theming": 6.5.16 + "@storybook/ui": 6.5.16 "@types/node": ^14.0.10 || ^16.0.0 "@types/webpack": ^4.41.26 autoprefixer: ^9.8.6 @@ -2109,30 +2109,30 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: bd676e9302d34445884376582c5d4cfe854ed8e64a9df6bbdcc76ef433534e65a23b1bf20c7225e85d21a27e1f1d905d0accd821fdef08afcd76569a8c8977c2 + checksum: 5e9137c390db00b4e166df3ca730eb1748f6bac92c841f3f75c37ad5277d6f5565f899de3bb0357fc51ce6821c8a8a8adba724e3dd7a3d1cc80816e09e5b7128 languageName: node linkType: hard "@storybook/builder-webpack5@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/builder-webpack5@npm:6.5.15" + version: 6.5.16 + resolution: "@storybook/builder-webpack5@npm:6.5.16" dependencies: "@babel/core": ^7.12.10 - "@storybook/addons": 6.5.15 - "@storybook/api": 6.5.15 - "@storybook/channel-postmessage": 6.5.15 - "@storybook/channels": 6.5.15 - "@storybook/client-api": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/components": 6.5.15 - "@storybook/core-common": 6.5.15 - "@storybook/core-events": 6.5.15 - "@storybook/node-logger": 6.5.15 - "@storybook/preview-web": 6.5.15 - "@storybook/router": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/channel-postmessage": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/node-logger": 6.5.16 + "@storybook/preview-web": 6.5.16 + "@storybook/router": 6.5.16 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.15 - "@storybook/theming": 6.5.15 + "@storybook/store": 6.5.16 + "@storybook/theming": 6.5.16 "@types/node": ^14.0.10 || ^16.0.0 babel-loader: ^8.0.0 babel-plugin-named-exports-order: ^0.0.2 @@ -2161,35 +2161,35 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 9bde333540c41c55e7687b1230fdd92926074df1a3be900b969d6d9eea240fb962b6fafb59e5c3fe83eeb42e0f0c835d2e226fc332f460f8aba0f4f49d833ffd + checksum: 0a6631f307c5ac56423860216d42ed95757906b004e949ed3dc2cce4f81d83d38de5cddbae65a0e65083eece6e4e8af05f6aabf5d78a80a7a7f62cf157a4e577 languageName: node linkType: hard -"@storybook/channel-postmessage@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/channel-postmessage@npm:6.5.15" +"@storybook/channel-postmessage@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/channel-postmessage@npm:6.5.16" dependencies: - "@storybook/channels": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 core-js: ^3.8.2 global: ^4.4.0 qs: ^6.10.0 telejson: ^6.0.8 - checksum: 7a09ba5bf163f8f5fef0bfd99eaab6c6391fa854e86bb44fcd0586bb73dd4ab5827cc23b7b38f993b81cbdb2ff8d58f81371be9301ddb3ea6f963ba560a42f09 + checksum: d3560d81dbf4710cc23b227c12be328d87e627581afcb5fec959f1e795fb2b5824db2a7f03a4ddcd185ec9a37a7025415d8bb43b7a245f2466395908eb3e9bc3 languageName: node linkType: hard -"@storybook/channel-websocket@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/channel-websocket@npm:6.5.15" +"@storybook/channel-websocket@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/channel-websocket@npm:6.5.16" dependencies: - "@storybook/channels": 6.5.15 - "@storybook/client-logger": 6.5.15 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 core-js: ^3.8.2 global: ^4.4.0 telejson: ^6.0.8 - checksum: c482b18b28f06644f684ed2b88ab53d6c5853925343e60a50a9bcfb2888123c4accfe30b51743905693e4d73c32c77e30d6dbaba377b486bee6e51faae39cf85 + checksum: 355c85f22d7cc65764871852debe347c43c3fe92d6a0caa64aecbe2dce78d4bf73b98e997099f9e4e7c204ad5821b979939b0700e446fa26478c1e1ba48e7380 languageName: node linkType: hard @@ -2204,28 +2204,28 @@ __metadata: languageName: node linkType: hard -"@storybook/channels@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/channels@npm:6.5.15" +"@storybook/channels@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/channels@npm:6.5.16" dependencies: core-js: ^3.8.2 ts-dedent: ^2.0.0 util-deprecate: ^1.0.2 - checksum: 7963c34246b3cc84bb6fb0965834110d9b839a5c32cced0756948e4e88fb8bf23a0d584723abbab6d30a6282fa1023017bb82eba68c23389652c77d8d33cb4f9 + checksum: 3d7f7bc19ed7b250976e00e02ab544408806b439106bed18a5db9815612f6c5df9bdf7c1a97b5a40ba3194184ebe7e4c75e2bca5496025d6b26afefa95cfccbd languageName: node linkType: hard -"@storybook/client-api@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/client-api@npm:6.5.15" +"@storybook/client-api@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/client-api@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/channel-postmessage": 6.5.15 - "@storybook/channels": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/channel-postmessage": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/store": 6.5.15 + "@storybook/store": 6.5.16 "@types/qs": ^6.9.5 "@types/webpack-env": ^1.16.0 core-js: ^3.8.2 @@ -2242,7 +2242,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 0c3b3f7febe16b00beb4aeafe79fd97cc4a6b4db868e37002856fc34878bc955336929362b3015ba1688b47131ca0b729c94ee70f69855da0e819dad6d48ee1b + checksum: a62276fa67d2c3cc766ea9145d3798c0c8ef3f9de9fb18e7c43d67e39226f47a2546c4319ccc6075545df65dc4fc65bdb97e904062daf426be6534767eacada6 languageName: node linkType: hard @@ -2256,23 +2256,23 @@ __metadata: languageName: node linkType: hard -"@storybook/client-logger@npm:6.5.15, @storybook/client-logger@npm:^6.4.0": - version: 6.5.15 - resolution: "@storybook/client-logger@npm:6.5.15" +"@storybook/client-logger@npm:6.5.16, @storybook/client-logger@npm:^6.4.0": + version: 6.5.16 + resolution: "@storybook/client-logger@npm:6.5.16" dependencies: core-js: ^3.8.2 global: ^4.4.0 - checksum: cee16aea089b60b33ad643bde5e0d62274230d38e2033f0bfd0780fc092bc24b5acff63a6c569c9db989e59b89518ec964d0665a51548450716c4c50d3a3e66e + checksum: 0a86959b1bacb1b893e282173b48afe9c857b8cdc67a47ad87a7f11ba7dbc15ebc4f0d05c07dffb988e0cd3e1de0f09f300ee06c66afe4c50e9be83aaed75971 languageName: node linkType: hard -"@storybook/components@npm:6.5.15, @storybook/components@npm:^6.0.0": - version: 6.5.15 - resolution: "@storybook/components@npm:6.5.15" +"@storybook/components@npm:6.5.16, @storybook/components@npm:^6.0.0": + version: 6.5.16 + resolution: "@storybook/components@npm:6.5.16" dependencies: - "@storybook/client-logger": 6.5.15 + "@storybook/client-logger": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/theming": 6.5.15 + "@storybook/theming": 6.5.16 core-js: ^3.8.2 memoizerific: ^1.11.3 qs: ^6.10.0 @@ -2281,24 +2281,24 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: c405643a002b7770567aba3f0e43cad975fbb0f728148f82c46c35b13958d3f6900eed619a60f22c0ee923c20828026d5f1c77cd950d21ebfd6cf57f8e64f791 + checksum: 1caf822bf1293ca043822f1c77f05c0f01631e8a61adad6bc4651ba9be78c8f4822ba0905e39c8feaa3fb44ae10422e9ccd3004348b18531fb82c54cfcea4fa9 languageName: node linkType: hard -"@storybook/core-client@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/core-client@npm:6.5.15" +"@storybook/core-client@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/core-client@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/channel-postmessage": 6.5.15 - "@storybook/channel-websocket": 6.5.15 - "@storybook/client-api": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/channel-postmessage": 6.5.16 + "@storybook/channel-websocket": 6.5.16 + "@storybook/client-api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/preview-web": 6.5.15 - "@storybook/store": 6.5.15 - "@storybook/ui": 6.5.15 + "@storybook/preview-web": 6.5.16 + "@storybook/store": 6.5.16 + "@storybook/ui": 6.5.16 airbnb-js-shims: ^2.2.1 ansi-to-html: ^0.6.11 core-js: ^3.8.2 @@ -2316,13 +2316,13 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 60f03d031fa87a1a116b0ccbffd2270b8d28757d89c17af6eb368224603dfd0d219fd5ac086bf859cb8bda0f80b444195c7df94c4486107b9c5a6fbab29c65ac + checksum: 467710777ddd740c431cf65035ecc489daae2fc5f4844a40b7339b806535e239140f40442a0e1d89356e107169c39d9e84d726c01982ed4609c043b6861e0778 languageName: node linkType: hard -"@storybook/core-common@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/core-common@npm:6.5.15" +"@storybook/core-common@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/core-common@npm:6.5.16" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-proposal-class-properties": ^7.12.1 @@ -2346,7 +2346,7 @@ __metadata: "@babel/preset-react": ^7.12.10 "@babel/preset-typescript": ^7.12.7 "@babel/register": ^7.12.1 - "@storybook/node-logger": 6.5.15 + "@storybook/node-logger": 6.5.16 "@storybook/semver": ^7.3.2 "@types/node": ^14.0.10 || ^16.0.0 "@types/pretty-hrtime": ^1.0.0 @@ -2363,7 +2363,7 @@ __metadata: glob: ^7.1.6 handlebars: ^4.7.7 interpret: ^2.2.0 - json5: ^2.1.3 + json5: ^2.2.3 lazy-universal-dotenv: ^3.0.1 picomatch: ^2.3.0 pkg-dir: ^5.0.0 @@ -2380,7 +2380,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 9c19c348137bea23295ff330d3a62d3551b6a8a2933f254f3f1cace4ef05e46b6c97e0cbca67cc5be45164e223d5ff52eced54b76564891c8a2dd085e3be4cc4 + checksum: 886a701876599939950c3c98e306b373cd026c7b995ca08d88475b3f35624a53763459d6b202728ec703e99126813a254b956c2d0fe7e85f99dcb5765a999b19 languageName: node linkType: hard @@ -2393,31 +2393,31 @@ __metadata: languageName: node linkType: hard -"@storybook/core-events@npm:6.5.15, @storybook/core-events@npm:^6.0.0": - version: 6.5.15 - resolution: "@storybook/core-events@npm:6.5.15" +"@storybook/core-events@npm:6.5.16, @storybook/core-events@npm:^6.0.0": + version: 6.5.16 + resolution: "@storybook/core-events@npm:6.5.16" dependencies: core-js: ^3.8.2 - checksum: 89916720933bc4de0b1f25c7cb1b8580d3cdd213b21a360f18ebd0b790cce2c641696282fee29bbc482ab2cc656271b2f0569f79559d90fb01fb16473421e79e + checksum: 1844bdabfb7828af7ddd54129fbb321bf65d8b65459eaac99c8f3f94c7c2f0ee000468362758076444083f863a3bc835ecd1e4f2128524eb5c00c8a576473bc9 languageName: node linkType: hard -"@storybook/core-server@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/core-server@npm:6.5.15" +"@storybook/core-server@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/core-server@npm:6.5.16" dependencies: "@discoveryjs/json-ext": ^0.5.3 - "@storybook/builder-webpack4": 6.5.15 - "@storybook/core-client": 6.5.15 - "@storybook/core-common": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/builder-webpack4": 6.5.16 + "@storybook/core-client": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/csf-tools": 6.5.15 - "@storybook/manager-webpack4": 6.5.15 - "@storybook/node-logger": 6.5.15 + "@storybook/csf-tools": 6.5.16 + "@storybook/manager-webpack4": 6.5.16 + "@storybook/node-logger": 6.5.16 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.15 - "@storybook/telemetry": 6.5.15 + "@storybook/store": 6.5.16 + "@storybook/telemetry": 6.5.16 "@types/node": ^14.0.10 || ^16.0.0 "@types/node-fetch": ^2.5.7 "@types/pretty-hrtime": ^1.0.0 @@ -2461,16 +2461,16 @@ __metadata: optional: true typescript: optional: true - checksum: 927085bd6e2c9cf756795760c5647ed5b40151e94a192e64313091fda8a06993541ecabb9c730cb01eb8baef0613b73ff63824812cfd7b8bbed3fd2ead8d1f18 + checksum: 2027adba39b2e0a5c3664241f48ec256a92866755aace96f3b8e2064b50237bbcd4e814bc58a1084006baae41c48d7d0eccefc9867d84e17d68d7f969e65f149 languageName: node linkType: hard -"@storybook/core@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/core@npm:6.5.15" +"@storybook/core@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/core@npm:6.5.16" dependencies: - "@storybook/core-client": 6.5.15 - "@storybook/core-server": 6.5.15 + "@storybook/core-client": 6.5.16 + "@storybook/core-server": 6.5.16 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -2482,13 +2482,13 @@ __metadata: optional: true typescript: optional: true - checksum: a7eca427b14a9c1f557d598e7ac599dc120d382d3d762437b3b6ebb4638b19a74e48f9e8e526365951ec623d10b82d67279226d7e6ff54ac095afee02992e666 + checksum: f1732338741692007230a351419ef3aa4e387810d7d0c0e6ffb1159e1de4d757199f2b543cf4f6413fc40acda514b908d2fd9b3e0d56e3f6cec1e3a82c2fcc10 languageName: node linkType: hard -"@storybook/csf-tools@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/csf-tools@npm:6.5.15" +"@storybook/csf-tools@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/csf-tools@npm:6.5.16" dependencies: "@babel/core": ^7.12.10 "@babel/generator": ^7.12.11 @@ -2509,7 +2509,7 @@ __metadata: peerDependenciesMeta: "@storybook/mdx2-csf": optional: true - checksum: d7faafd175b232bb8fa6008ae6db5a40619a158ed7556686649dee665ac0cbbdb3cc404d2b9c0314ba7783c3f5baf1d87788f324d24136bd0f8cc671d573208b + checksum: ee71a47d90186c35fc1dbcb6ece2888ff4d730bde823bb1bd242d802b74045b482d2c469f3a91687b691b6f828ce449b182896d1912033846b9746457ee960ba languageName: node linkType: hard @@ -2522,18 +2522,18 @@ __metadata: languageName: node linkType: hard -"@storybook/docs-tools@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/docs-tools@npm:6.5.15" +"@storybook/docs-tools@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/docs-tools@npm:6.5.16" dependencies: "@babel/core": ^7.12.10 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/store": 6.5.15 + "@storybook/store": 6.5.16 core-js: ^3.8.2 doctrine: ^3.0.0 lodash: ^4.17.21 regenerator-runtime: ^0.13.7 - checksum: 051239a82cff47dbc52fae112c4c144d6e103cbb169c239b0a99fb0cb3e82fba2e460c9487469fafc19ee81ef2ecc33d59b05457c3c74375685de0f537460071 + checksum: 6351c5b1cbe5820f0f0dfcc3e4e7da8cca3c8d73a06c5803e65cb86e9e81ccbae53cec8e1b579af0ac9a5bbb6d4b6ac03ffe26af2220dc5dfe8f065067f0e2d7 languageName: node linkType: hard @@ -2550,19 +2550,19 @@ __metadata: languageName: node linkType: hard -"@storybook/manager-webpack4@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/manager-webpack4@npm:6.5.15" +"@storybook/manager-webpack4@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/manager-webpack4@npm:6.5.16" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-transform-template-literals": ^7.12.1 "@babel/preset-react": ^7.12.10 - "@storybook/addons": 6.5.15 - "@storybook/core-client": 6.5.15 - "@storybook/core-common": 6.5.15 - "@storybook/node-logger": 6.5.15 - "@storybook/theming": 6.5.15 - "@storybook/ui": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/core-client": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/node-logger": 6.5.16 + "@storybook/theming": 6.5.16 + "@storybook/ui": 6.5.16 "@types/node": ^14.0.10 || ^16.0.0 "@types/webpack": ^4.41.26 babel-loader: ^8.0.0 @@ -2595,23 +2595,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: de7b2624bf44cf4eb59839ac756e9e28715caae1d5efdfbd1e2cefb55a385cf80f3a651a29ed75a905f825d6b299ba46c3cd71068eecb3b985b5a315ed03470b + checksum: 873c871c822ecde30fbd95e9517549a18c5bb2de46d6160d6dcd7c1b5635fda2073b5bc4bd4d87e72de6e8df8bccf39b81f062e07cd7a23ffb4b43293e488fbb languageName: node linkType: hard "@storybook/manager-webpack5@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/manager-webpack5@npm:6.5.15" + version: 6.5.16 + resolution: "@storybook/manager-webpack5@npm:6.5.16" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-transform-template-literals": ^7.12.1 "@babel/preset-react": ^7.12.10 - "@storybook/addons": 6.5.15 - "@storybook/core-client": 6.5.15 - "@storybook/core-common": 6.5.15 - "@storybook/node-logger": 6.5.15 - "@storybook/theming": 6.5.15 - "@storybook/ui": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/core-client": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/node-logger": 6.5.16 + "@storybook/theming": 6.5.16 + "@storybook/ui": 6.5.16 "@types/node": ^14.0.10 || ^16.0.0 babel-loader: ^8.0.0 case-sensitive-paths-webpack-plugin: ^2.3.0 @@ -2641,7 +2641,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 99f18a752230fa6360b216e3a7f85c70fe92baf01677571653b4f9fd62735d9ab12a0b91e4ae7eabd66b03f37e0b58f01fad8e5880a8e2012a98f6aedc944da1 + checksum: 1349c6b2af9d0cebc3c35c929e2ea0f9ff8d12f7a04c30126160d9c89a45b92412218304abda9126cf96303a2d73fb288a689a191fec12b0189f19e5f2032977 languageName: node linkType: hard @@ -2664,29 +2664,29 @@ __metadata: languageName: node linkType: hard -"@storybook/node-logger@npm:6.5.15, @storybook/node-logger@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/node-logger@npm:6.5.15" +"@storybook/node-logger@npm:6.5.16, @storybook/node-logger@npm:^6.5.9": + version: 6.5.16 + resolution: "@storybook/node-logger@npm:6.5.16" dependencies: "@types/npmlog": ^4.1.2 chalk: ^4.1.0 core-js: ^3.8.2 npmlog: ^5.0.1 pretty-hrtime: ^1.0.3 - checksum: 9c01127d3b57db7a85759d2f179afec0e1207c0754e80e22472e73468f831e1dafa2a5bf1047e54f92d47b5103325c157c14655208a6ddcdb8f9e7ee0b256e48 + checksum: 4ae47c03b6cec6b820e0e482e6f6675bf745fca5c124eb919240c0339b9f4a1b110c8fde7c5ddbc1748d3992773c61d37ba1f5c489b42279cf03517d4e1d51c5 languageName: node linkType: hard -"@storybook/preview-web@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/preview-web@npm:6.5.15" +"@storybook/preview-web@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/preview-web@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/channel-postmessage": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/channel-postmessage": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/store": 6.5.15 + "@storybook/store": 6.5.16 ansi-to-html: ^0.6.11 core-js: ^3.8.2 global: ^4.4.0 @@ -2700,7 +2700,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: fc178af772f39fcfe1b9926bf62bd7642984080442e8f7f2a0c6fe421513f9930ff5ccfea302d212199adf418c4bf9fc498ff65c30ad6e1cdd1b485a6d92b190 + checksum: 6161c96e9ee459ef93c3d972374ce339ae57d0c5fa25730007484e4824f79a34814110431db97031107558e5ce41259710f8a54564e8975db0215b78c5572a1b languageName: node linkType: hard @@ -2723,22 +2723,22 @@ __metadata: linkType: hard "@storybook/react@npm:^6.5.9": - version: 6.5.15 - resolution: "@storybook/react@npm:6.5.15" + version: 6.5.16 + resolution: "@storybook/react@npm:6.5.16" dependencies: "@babel/preset-flow": ^7.12.1 "@babel/preset-react": ^7.12.10 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.3 - "@storybook/addons": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/core": 6.5.15 - "@storybook/core-common": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core": 6.5.16 + "@storybook/core-common": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/docs-tools": 6.5.15 - "@storybook/node-logger": 6.5.15 + "@storybook/docs-tools": 6.5.16 + "@storybook/node-logger": 6.5.16 "@storybook/react-docgen-typescript-plugin": 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.15 + "@storybook/store": 6.5.16 "@types/estree": ^0.0.51 "@types/node": ^14.14.20 || ^16.0.0 "@types/webpack-env": ^1.16.0 @@ -2783,7 +2783,7 @@ __metadata: build-storybook: bin/build.js start-storybook: bin/index.js storybook-server: bin/index.js - checksum: c36f9a2401633b9632e5fce05bb43e8ea0a5338c8f0dc9e0da5eb87c7cfd1a3ef819124499679a3c797fbbe8278f03794d757563f388136a349e3815747036ae + checksum: c5396e748ef13acdb2590dc15ff0b3d95d3599abd0c372786d707164d3f71e46836240195dcd6f4bce6f90d2792602f6d31373fc87e069ef3c73a63d1e9a1289 languageName: node linkType: hard @@ -2803,11 +2803,11 @@ __metadata: languageName: node linkType: hard -"@storybook/router@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/router@npm:6.5.15" +"@storybook/router@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/router@npm:6.5.16" dependencies: - "@storybook/client-logger": 6.5.15 + "@storybook/client-logger": 6.5.16 core-js: ^3.8.2 memoizerific: ^1.11.3 qs: ^6.10.0 @@ -2815,7 +2815,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: d5ac1ac0d161e53764411dc84febed3819c5cefe669f2933434bcdc25bf011f89d2df2a504af8bf77f454e6598a74c794a17d01aad734c6ebe28cc13c490fff9 + checksum: 2812b93997026b1d85f02072d04f18e98e24de288efb73402f8d15ececd390e13dc620ef011268e09986c629f497ffa03230c2431e89b4e37c01b70761be2c6d languageName: node linkType: hard @@ -2831,12 +2831,12 @@ __metadata: languageName: node linkType: hard -"@storybook/source-loader@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/source-loader@npm:6.5.15" +"@storybook/source-loader@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/source-loader@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/client-logger": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/client-logger": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 core-js: ^3.8.2 estraverse: ^5.2.0 @@ -2848,17 +2848,17 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 2330d2b16f097c4be4b0eccc466ae0142c2e04198acc529204d80c1005c1c32b6da8313661b10e641020efdfe4f1dd4f0ac67bc2c3797ae49383815f668ede5d + checksum: a299acdd6f36add3222ef294e1118b7b1f38c2cd2b4648ebf9e1803a3ccf532c147dbe643a527915b570eb3ce36c4a17ca2b3566fa58a2a0a7821f0849ec3e07 languageName: node linkType: hard -"@storybook/store@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/store@npm:6.5.15" +"@storybook/store@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/store@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/core-events": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 "@storybook/csf": 0.0.2--canary.4566f4d.1 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 @@ -2874,16 +2874,16 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 918c3ed8c7a55ae4bf8bcb3a108d99a9d077c951b3f386cb0f8939d2eed7c9a9a2000075b341d5c934c0308c24287fc5cd110042a384411c25cec7632dfa5abb + checksum: f438fb020af240e23348742b2936a326bef1f7ffd489fe9f39cfd516310ab592a11609205fdacd11090b0c0b6bc72c75dff986085a6a97acc5efa64829a49309 languageName: node linkType: hard -"@storybook/telemetry@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/telemetry@npm:6.5.15" +"@storybook/telemetry@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/telemetry@npm:6.5.16" dependencies: - "@storybook/client-logger": 6.5.15 - "@storybook/core-common": 6.5.15 + "@storybook/client-logger": 6.5.16 + "@storybook/core-common": 6.5.16 chalk: ^4.1.0 core-js: ^3.8.2 detect-package-manager: ^2.0.1 @@ -2894,7 +2894,7 @@ __metadata: nanoid: ^3.3.1 read-pkg-up: ^7.0.1 regenerator-runtime: ^0.13.7 - checksum: aebb83186ff7308e21185a7152b27aed43f6d3967a7253ac94e3d4c1bce4935c471500c37d195e03f98953944812d2b24518d4147704e7ffb211430b27a2354e + checksum: 21eef590b04db8ee85b0b1d875d8646e26492b3e90538a248314f92d6ab0642ec65db09c5d2bc0d7f547f0fa6b83ca4442bdc115b400861360e02d8cf179497e languageName: node linkType: hard @@ -2926,34 +2926,34 @@ __metadata: languageName: node linkType: hard -"@storybook/theming@npm:6.5.15, @storybook/theming@npm:^6.0.0": - version: 6.5.15 - resolution: "@storybook/theming@npm:6.5.15" +"@storybook/theming@npm:6.5.16, @storybook/theming@npm:^6.0.0": + version: 6.5.16 + resolution: "@storybook/theming@npm:6.5.16" dependencies: - "@storybook/client-logger": 6.5.15 + "@storybook/client-logger": 6.5.16 core-js: ^3.8.2 memoizerific: ^1.11.3 regenerator-runtime: ^0.13.7 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 84d09b6bdd0a00246b207ef4307fc0ecbb5861792547a070ed45470335c323f18ba607cd1d3f0a5cea7e979dd73315cdb091548fe70e5946c1593d8c691be7ba + checksum: 349affa5c5208240291a5d24c73d852e220bfaf36b8fda70564aec1cac6070248ce7566ccb755c55a6ce0844ab2bbfd55881f6f788240b38cb407714e393c6f3 languageName: node linkType: hard -"@storybook/ui@npm:6.5.15": - version: 6.5.15 - resolution: "@storybook/ui@npm:6.5.15" +"@storybook/ui@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/ui@npm:6.5.16" dependencies: - "@storybook/addons": 6.5.15 - "@storybook/api": 6.5.15 - "@storybook/channels": 6.5.15 - "@storybook/client-logger": 6.5.15 - "@storybook/components": 6.5.15 - "@storybook/core-events": 6.5.15 - "@storybook/router": 6.5.15 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/router": 6.5.16 "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.5.15 + "@storybook/theming": 6.5.16 core-js: ^3.8.2 memoizerific: ^1.11.3 qs: ^6.10.0 @@ -2962,94 +2962,94 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 8b75290c65a6a2beb3db4157ed95ce74dab06d5499dc3fc7a848fa5c9fc0f506bf6533638bb50c3a1f27acfeaa6ade0acdde8beede2241eba8d577965d85d299 + checksum: bfebcf4d56dc5fd6024eaa08fe50aecc3c348670b7c0ec6b467680d64d525421580b9c98839bcaf1e2a9e69b78478a21c9943a9a392b49a0405b4784038b2eba languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-darwin-arm64@npm:1.3.26" +"@swc/core-darwin-arm64@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-darwin-arm64@npm:1.3.36" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-darwin-x64@npm:1.3.26" +"@swc/core-darwin-x64@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-darwin-x64@npm:1.3.36" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.26" +"@swc/core-linux-arm-gnueabihf@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.36" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.26" +"@swc/core-linux-arm64-gnu@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.36" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.26" +"@swc/core-linux-arm64-musl@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.36" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.26" +"@swc/core-linux-x64-gnu@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.36" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-x64-musl@npm:1.3.26" +"@swc/core-linux-x64-musl@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-x64-musl@npm:1.3.36" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.26" +"@swc/core-win32-arm64-msvc@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.36" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.26" +"@swc/core-win32-ia32-msvc@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.36" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.26" +"@swc/core-win32-x64-msvc@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.36" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.3.9": - version: 1.3.26 - resolution: "@swc/core@npm:1.3.26" + version: 1.3.36 + resolution: "@swc/core@npm:1.3.36" dependencies: - "@swc/core-darwin-arm64": 1.3.26 - "@swc/core-darwin-x64": 1.3.26 - "@swc/core-linux-arm-gnueabihf": 1.3.26 - "@swc/core-linux-arm64-gnu": 1.3.26 - "@swc/core-linux-arm64-musl": 1.3.26 - "@swc/core-linux-x64-gnu": 1.3.26 - "@swc/core-linux-x64-musl": 1.3.26 - "@swc/core-win32-arm64-msvc": 1.3.26 - "@swc/core-win32-ia32-msvc": 1.3.26 - "@swc/core-win32-x64-msvc": 1.3.26 + "@swc/core-darwin-arm64": 1.3.36 + "@swc/core-darwin-x64": 1.3.36 + "@swc/core-linux-arm-gnueabihf": 1.3.36 + "@swc/core-linux-arm64-gnu": 1.3.36 + "@swc/core-linux-arm64-musl": 1.3.36 + "@swc/core-linux-x64-gnu": 1.3.36 + "@swc/core-linux-x64-musl": 1.3.36 + "@swc/core-win32-arm64-msvc": 1.3.36 + "@swc/core-win32-ia32-msvc": 1.3.36 + "@swc/core-win32-x64-msvc": 1.3.36 dependenciesMeta: "@swc/core-darwin-arm64": optional: true @@ -3071,7 +3071,7 @@ __metadata: optional: true "@swc/core-win32-x64-msvc": optional: true - checksum: 656a2197bd8bd50b112077e8c7f9156527ec66cb06fed2cefb9e2610e3cde13e8416783f84edda67b9af4e27d6b169bc1a447d7c6c73cf64a1b3ec428b0737ab + checksum: 36e96e86d1ab8fd84eb51e7264a7ae781ee7e507d36b9ccc028d7f31a2ccf2949f074656e34139c970196774d24d55a54c3aa5c53d04eb6e3569c71cbfed0c0d languageName: node linkType: hard @@ -6783,9 +6783,9 @@ __metadata: linkType: hard "http-cache-semantics@npm:^4.1.0": - version: 4.1.0 - resolution: "http-cache-semantics@npm:4.1.0" - checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 languageName: node linkType: hard @@ -7606,12 +7606,12 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.1.2, json5@npm:^2.1.3, json5@npm:^2.2.1": - version: 2.2.1 - resolution: "json5@npm:2.2.1" +"json5@npm:^2.1.2, json5@npm:^2.2.1, json5@npm:^2.2.3": + version: 2.2.3 + resolution: "json5@npm:2.2.3" bin: json5: lib/cli.js - checksum: 74b8a23b102a6f2bf2d224797ae553a75488b5adbaee9c9b6e5ab8b510a2fc6e38f876d4c77dea672d4014a44b2399e15f2051ac2b37b87f74c0c7602003543b + checksum: 2a7436a93393830bce797d4626275152e37e877b265e94ca69c99e3d20c2b9dab021279146a39cdb700e71b2dd32a4cebd1514cd57cee102b1af906ce5040349 languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index 974db36e32..e11681d129 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,14 +22,14 @@ __metadata: linkType: hard "@apidevtools/json-schema-ref-parser@npm:^9.0.6": - version: 9.1.0 - resolution: "@apidevtools/json-schema-ref-parser@npm:9.1.0" + version: 9.1.2 + resolution: "@apidevtools/json-schema-ref-parser@npm:9.1.2" dependencies: "@jsdevtools/ono": ^7.1.3 "@types/json-schema": ^7.0.6 call-me-maybe: ^1.0.1 js-yaml: ^4.1.0 - checksum: 135569d905cd5cd3652fe850ff3a07bfbbfb1585bb20139710e8a865f0bd9fa1c8bbe8122cce11928ce00fc618b8b9f61ccd7b62827654c9b7e2853789ee0e4a + checksum: 5bd6885db0fd6633879bb4638b7a3aead6b061cb6422083c6be505ee6873be54e3376380df164c73edd8901d4145a9bfe9bc0b008a568fd8b0626b1df96fae8f languageName: node linkType: hard @@ -217,14 +217,16 @@ __metadata: languageName: node linkType: hard -"@asyncapi/avro-schema-parser@npm:^0.3.0": - version: 0.3.0 - resolution: "@asyncapi/avro-schema-parser@npm:0.3.0" - checksum: 3086bc16a634600fe4d7c83f52f9558684c0d79193958fcb9275cacfb550700eaa718aa9ed1d3c89d1d77253f67e0f33ea91e137ef08aa722d67ded9373bf142 +"@asyncapi/avro-schema-parser@npm:^1.1.0": + version: 1.1.0 + resolution: "@asyncapi/avro-schema-parser@npm:1.1.0" + dependencies: + avsc: ^5.7.3 + checksum: 31bb6285a49f255e809088164ac4638103b2ec3e761c9c85be116af8abd9fe429626ba79ba6b3899d3823a77216741d9d793841e600145f2956328f3ce45a0e0 languageName: node linkType: hard -"@asyncapi/openapi-schema-parser@npm:^2.0.0": +"@asyncapi/openapi-schema-parser@npm:^2.0.1": version: 2.0.1 resolution: "@asyncapi/openapi-schema-parser@npm:2.0.1" dependencies: @@ -233,12 +235,12 @@ __metadata: languageName: node linkType: hard -"@asyncapi/parser@npm:^1.17.0": - version: 1.17.0 - resolution: "@asyncapi/parser@npm:1.17.0" +"@asyncapi/parser@npm:^1.18.0": + version: 1.18.0 + resolution: "@asyncapi/parser@npm:1.18.0" dependencies: "@apidevtools/json-schema-ref-parser": ^9.0.6 - "@asyncapi/specs": ^3.2.0 + "@asyncapi/specs": ^4.1.0 "@fmvilas/pseudo-yaml-ast": ^0.3.1 ajv: ^6.10.1 js-yaml: ^3.13.1 @@ -246,147 +248,138 @@ __metadata: lodash.clonedeep: ^4.5.0 node-fetch: ^2.6.0 tiny-merge-patch: ^0.1.2 - checksum: 6ac0bd3c9c90abf331cfdfb054dab047cf0324ee803633c2abc6dad96b12c64d033e4fb94907906adfb521ba16e9b23083b2a6acc7719ccd22154a7867046a95 + checksum: 7e08cae52990ce97299170faf99aaf27a8e2e02e6b03df28518d256f5687f394c26fd0a4aba235514ed4e3c2fd081a79321a489c20987ed22122afdb0b9ea0dd languageName: node linkType: hard -"@asyncapi/react-component@npm:1.0.0-next.44": - version: 1.0.0-next.44 - resolution: "@asyncapi/react-component@npm:1.0.0-next.44" +"@asyncapi/react-component@npm:1.0.0-next.47": + version: 1.0.0-next.47 + resolution: "@asyncapi/react-component@npm:1.0.0-next.47" dependencies: - "@asyncapi/avro-schema-parser": ^0.3.0 - "@asyncapi/openapi-schema-parser": ^2.0.0 - "@asyncapi/parser": ^1.17.0 + "@asyncapi/avro-schema-parser": ^1.1.0 + "@asyncapi/openapi-schema-parser": ^2.0.1 + "@asyncapi/parser": ^1.18.0 highlight.js: ^10.7.2 isomorphic-dompurify: ^0.13.0 marked: ^4.0.14 openapi-sampler: ^1.2.1 - react-icons: ^4.4.0 use-resize-observer: ^8.0.0 peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 1d96db3bc6a4655884969a7639467836376c028df1a00f8930adf4e1b6ecb43e6a2d61df5c68e63d6c105d319fecb3f2ac0e4146274a81a874fbde17f4dbac1c + checksum: 837b6378f1188945a0e92d93efe71691a8dad120fad5624b58b325ec86c51435b874464ec0c47308ffacda9ee4d77ad8217881584bd74081f6db2211f09cc60c languageName: node linkType: hard -"@asyncapi/specs@npm:^3.2.0": - version: 3.2.0 - resolution: "@asyncapi/specs@npm:3.2.0" - checksum: 09971262aefc8844ab3e7c0c3652711862ac562dd5d614f23b496185690430a81df8e50eddba657f4141e0fd9548ef622fe6c20f4e3dec8054be23f774798335 - languageName: node - linkType: hard - -"@aws-crypto/crc32@npm:2.0.0": - version: 2.0.0 - resolution: "@aws-crypto/crc32@npm:2.0.0" +"@asyncapi/specs@npm:^4.1.0": + version: 4.1.0 + resolution: "@asyncapi/specs@npm:4.1.0" dependencies: - "@aws-crypto/util": ^2.0.0 - "@aws-sdk/types": ^3.1.0 - tslib: ^1.11.1 - checksum: 88ab906da8304a430c655496e363835f3c7ca870db0dec50bb9d53ed0f446337de60c85ba7baa4528c8363bee708474785649262ebfde23a1e099eb69318b53e + "@types/json-schema": ^7.0.11 + checksum: 6e95f3c1ef7267480cdfc69f5a015f63b9101874289e31843a629346a3ea07490e3043b296a089bf3458e877c664d83d4b4738dcb53d37d7e13b75c7bc08c879 languageName: node linkType: hard -"@aws-crypto/crc32c@npm:2.0.0": - version: 2.0.0 - resolution: "@aws-crypto/crc32c@npm:2.0.0" +"@aws-crypto/crc32@npm:3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/crc32@npm:3.0.0" dependencies: - "@aws-crypto/util": ^2.0.0 - "@aws-sdk/types": ^3.1.0 + "@aws-crypto/util": ^3.0.0 + "@aws-sdk/types": ^3.222.0 tslib: ^1.11.1 - checksum: 776e1e61b3bde018b815d3973774a4ec3cd88c282046e49bb5a2625ac7db923068aad708a8e4c21b62a80e50a5c58324a32621b9ba82b7b2ecab66370c4fe893 + checksum: 9fdb3e837fc54119b017ea34fd0a6d71d2c88075d99e1e818a5158e0ad30ced67ddbcc423a11ceeef6cc465ab5ffd91830acab516470b48237ca7abd51be9642 languageName: node linkType: hard -"@aws-crypto/ie11-detection@npm:^2.0.0": - version: 2.0.2 - resolution: "@aws-crypto/ie11-detection@npm:2.0.2" +"@aws-crypto/crc32c@npm:3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/crc32c@npm:3.0.0" + dependencies: + "@aws-crypto/util": ^3.0.0 + "@aws-sdk/types": ^3.222.0 + tslib: ^1.11.1 + checksum: 0a116b5d1c5b09a3dde65aab04a07b32f543e87b68f2d175081e3f4a1a17502343f223d691dd883ace1ddce65cd40093673e7c7415dcd99062202ba87ffb4038 + languageName: node + linkType: hard + +"@aws-crypto/ie11-detection@npm:^3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/ie11-detection@npm:3.0.0" dependencies: tslib: ^1.11.1 - checksum: 713293deea8eefd3ab43dc05e62228571d27754e7293f8ec2fd8a0c693fbbfc55213e6599387776e3cdbc951965dc62e24e92b9c4a853e4a50d00ae6a9f6b2bd + checksum: 299b2ddd46eddac1f2d54d91386ceb37af81aef8a800669281c73d634ed17fd855dcfb8b3157f2879344b93a2666a6d602550eb84b71e4d7868100ad6da8f803 languageName: node linkType: hard -"@aws-crypto/sha1-browser@npm:2.0.0": - version: 2.0.0 - resolution: "@aws-crypto/sha1-browser@npm:2.0.0" +"@aws-crypto/sha1-browser@npm:3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/sha1-browser@npm:3.0.0" dependencies: - "@aws-crypto/ie11-detection": ^2.0.0 - "@aws-crypto/supports-web-crypto": ^2.0.0 - "@aws-sdk/types": ^3.1.0 + "@aws-crypto/ie11-detection": ^3.0.0 + "@aws-crypto/supports-web-crypto": ^3.0.0 + "@aws-crypto/util": ^3.0.0 + "@aws-sdk/types": ^3.222.0 "@aws-sdk/util-locate-window": ^3.0.0 "@aws-sdk/util-utf8-browser": ^3.0.0 tslib: ^1.11.1 - checksum: 72c0b24800cd79328fef934553e7a5d5929c90877d7e9a661614542dd29d0d99ee1594bd59fc028ee9ec595d77df56645a396c6336cff3c7784a564302d4a254 + checksum: 78c379e105a0c4e7b2ed745dffd8f55054d7dde8b350b61de682bbc3cd081a50e2f87861954fa9cd53c7ea711ebca1ca0137b14cb36483efc971f60f573cf129 languageName: node linkType: hard -"@aws-crypto/sha256-browser@npm:2.0.0": - version: 2.0.0 - resolution: "@aws-crypto/sha256-browser@npm:2.0.0" +"@aws-crypto/sha256-browser@npm:3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/sha256-browser@npm:3.0.0" dependencies: - "@aws-crypto/ie11-detection": ^2.0.0 - "@aws-crypto/sha256-js": ^2.0.0 - "@aws-crypto/supports-web-crypto": ^2.0.0 - "@aws-crypto/util": ^2.0.0 - "@aws-sdk/types": ^3.1.0 + "@aws-crypto/ie11-detection": ^3.0.0 + "@aws-crypto/sha256-js": ^3.0.0 + "@aws-crypto/supports-web-crypto": ^3.0.0 + "@aws-crypto/util": ^3.0.0 + "@aws-sdk/types": ^3.222.0 "@aws-sdk/util-locate-window": ^3.0.0 "@aws-sdk/util-utf8-browser": ^3.0.0 tslib: ^1.11.1 - checksum: 7bc1ff042d0c53a46c0fc3824bd97fb3ed1df7dc030b8a995889471052860b8c8ade469c97866fafd8249a3144d0f48b0f1054f357e2b403606009381c4b8f0e + checksum: ca89456bf508db2e08060a7f656460db97ac9a15b11e39d6fa7665e2b156508a1758695bff8e82d0a00178d6ac5c36f35eb4bcfac2e48621265224ca14a19bd2 languageName: node linkType: hard -"@aws-crypto/sha256-js@npm:2.0.0": - version: 2.0.0 - resolution: "@aws-crypto/sha256-js@npm:2.0.0" +"@aws-crypto/sha256-js@npm:3.0.0, @aws-crypto/sha256-js@npm:^3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/sha256-js@npm:3.0.0" dependencies: - "@aws-crypto/util": ^2.0.0 - "@aws-sdk/types": ^3.1.0 + "@aws-crypto/util": ^3.0.0 + "@aws-sdk/types": ^3.222.0 tslib: ^1.11.1 - checksum: e4abf9baec6bed19d380f92a999a41ac5bdd8890dfd45971d29054c298854c5b7087e7de633413f2e64618ef8238ccf4c0b75797c73063c74bbba3cb5d8b2581 + checksum: 644ded32ea310237811afae873d3c7320739cb6f6cc39dced9c94801379e68e5ee2cca0c34f0384793fa9e750a7e0a5e2468f95754bd08e6fd72ab833c8fe23c languageName: node linkType: hard -"@aws-crypto/sha256-js@npm:^2.0.0": - version: 2.0.2 - resolution: "@aws-crypto/sha256-js@npm:2.0.2" - dependencies: - "@aws-crypto/util": ^2.0.2 - "@aws-sdk/types": ^3.110.0 - tslib: ^1.11.1 - checksum: 9125ec65a2b05fce908ac2289ba97b995a299f2d717684804211df8e8bcffd8cd9b8861582240655b88f2255c46fcee34026f75c057ffb22f44b6a76cd43f65a - languageName: node - linkType: hard - -"@aws-crypto/supports-web-crypto@npm:^2.0.0": - version: 2.0.2 - resolution: "@aws-crypto/supports-web-crypto@npm:2.0.2" +"@aws-crypto/supports-web-crypto@npm:^3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/supports-web-crypto@npm:3.0.0" dependencies: tslib: ^1.11.1 - checksum: 03d04d29292dc1b76db9bc6becd05f52fa79adee0ec084f971b0767f7e73250dd0422bea57636015f8c27f38aefcd1d9c58800a4749cf35339296c8d670f3ccb + checksum: 35479a1558db9e9a521df6877a99f95670e972c602f2a0349303477e5d638a5baf569fb037c853710e382086e6fd77e8ed58d3fb9b49f6e1186a9d26ce7be006 languageName: node linkType: hard -"@aws-crypto/util@npm:^2.0.0, @aws-crypto/util@npm:^2.0.2": - version: 2.0.2 - resolution: "@aws-crypto/util@npm:2.0.2" +"@aws-crypto/util@npm:^3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/util@npm:3.0.0" dependencies: - "@aws-sdk/types": ^3.110.0 + "@aws-sdk/types": ^3.222.0 "@aws-sdk/util-utf8-browser": ^3.0.0 tslib: ^1.11.1 - checksum: 13cb33a39005b09c062398d361043c2224bc8ba42b1432bad52e15bc4bf9ffad4facdddc394b3cc71b3fb8d86a7ec325fd1afa107b5fde0dab84a7e32d311d7f + checksum: d29d5545048721aae3d60b236708535059733019a105f8a64b4e4a8eab7cf8dde1546dc56bff7de20d36140a4d1f0f4693e639c5732a7059273a7b1e56354776 languageName: node linkType: hard -"@aws-sdk/abort-controller@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/abort-controller@npm:3.226.0" +"@aws-sdk/abort-controller@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/abort-controller@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 44045b60c7697ed76bcbfbe3f7f4bb019f139d2337e77d8ce79d98bca17c1245e2d33934bba04fe8e2d462b729124ba4414a589ed9275c07f2bfefd3a0850184 + checksum: 298974b97641732c74272dd645b55c15aaefd891b5a868c279ae2abe3c3e0753e7e6f218e8c3712196ca2d4f19796623cb48f9e9816b5bbbc4eb38247d07d0fe languageName: node linkType: hard @@ -409,483 +402,477 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-cognito-identity@npm:3.245.0": - version: 3.245.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.245.0" +"@aws-sdk/client-cognito-identity@npm:3.276.0": + version: 3.276.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.276.0" dependencies: - "@aws-crypto/sha256-browser": 2.0.0 - "@aws-crypto/sha256-js": 2.0.0 - "@aws-sdk/client-sts": 3.245.0 - "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/credential-provider-node": 3.245.0 - "@aws-sdk/fetch-http-handler": 3.226.0 - "@aws-sdk/hash-node": 3.226.0 - "@aws-sdk/invalid-dependency": 3.226.0 - "@aws-sdk/middleware-content-length": 3.226.0 - "@aws-sdk/middleware-endpoint": 3.226.0 - "@aws-sdk/middleware-host-header": 3.226.0 - "@aws-sdk/middleware-logger": 3.226.0 - "@aws-sdk/middleware-recursion-detection": 3.226.0 - "@aws-sdk/middleware-retry": 3.235.0 - "@aws-sdk/middleware-serde": 3.226.0 - "@aws-sdk/middleware-signing": 3.226.0 - "@aws-sdk/middleware-stack": 3.226.0 - "@aws-sdk/middleware-user-agent": 3.226.0 - "@aws-sdk/node-config-provider": 3.226.0 - "@aws-sdk/node-http-handler": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/smithy-client": 3.234.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/url-parser": 3.226.0 + "@aws-crypto/sha256-browser": 3.0.0 + "@aws-crypto/sha256-js": 3.0.0 + "@aws-sdk/client-sts": 3.276.0 + "@aws-sdk/config-resolver": 3.272.0 + "@aws-sdk/credential-provider-node": 3.272.0 + "@aws-sdk/fetch-http-handler": 3.272.0 + "@aws-sdk/hash-node": 3.272.0 + "@aws-sdk/invalid-dependency": 3.272.0 + "@aws-sdk/middleware-content-length": 3.272.0 + "@aws-sdk/middleware-endpoint": 3.272.0 + "@aws-sdk/middleware-host-header": 3.272.0 + "@aws-sdk/middleware-logger": 3.272.0 + "@aws-sdk/middleware-recursion-detection": 3.272.0 + "@aws-sdk/middleware-retry": 3.272.0 + "@aws-sdk/middleware-serde": 3.272.0 + "@aws-sdk/middleware-signing": 3.272.0 + "@aws-sdk/middleware-stack": 3.272.0 + "@aws-sdk/middleware-user-agent": 3.272.0 + "@aws-sdk/node-config-provider": 3.272.0 + "@aws-sdk/node-http-handler": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/smithy-client": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/url-parser": 3.272.0 "@aws-sdk/util-base64": 3.208.0 "@aws-sdk/util-body-length-browser": 3.188.0 "@aws-sdk/util-body-length-node": 3.208.0 - "@aws-sdk/util-defaults-mode-browser": 3.234.0 - "@aws-sdk/util-defaults-mode-node": 3.234.0 - "@aws-sdk/util-endpoints": 3.245.0 - "@aws-sdk/util-retry": 3.229.0 - "@aws-sdk/util-user-agent-browser": 3.226.0 - "@aws-sdk/util-user-agent-node": 3.226.0 - "@aws-sdk/util-utf8-browser": 3.188.0 - "@aws-sdk/util-utf8-node": 3.208.0 + "@aws-sdk/util-defaults-mode-browser": 3.272.0 + "@aws-sdk/util-defaults-mode-node": 3.272.0 + "@aws-sdk/util-endpoints": 3.272.0 + "@aws-sdk/util-retry": 3.272.0 + "@aws-sdk/util-user-agent-browser": 3.272.0 + "@aws-sdk/util-user-agent-node": 3.272.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: 4142ba1b7a3c11f785d5da527a743d6a9352f6f97ae983e6f3ee0abf5a1cd755c2196981cbc2f6052686bf820a68f062117c682a5126e55acaacebc5cab862e6 + checksum: ff79c1c727d956e03510307b001c24534d522b327871b2b0a1d39120e6d6717e17d7b9c2b7a6d28c3e5c3f41cd8ff28e5150ddd1fda9bff0c239fdfef0a78e00 languageName: node linkType: hard "@aws-sdk/client-s3@npm:^3.208.0": - version: 3.245.0 - resolution: "@aws-sdk/client-s3@npm:3.245.0" + version: 3.276.0 + resolution: "@aws-sdk/client-s3@npm:3.276.0" dependencies: - "@aws-crypto/sha1-browser": 2.0.0 - "@aws-crypto/sha256-browser": 2.0.0 - "@aws-crypto/sha256-js": 2.0.0 - "@aws-sdk/client-sts": 3.245.0 - "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/credential-provider-node": 3.245.0 - "@aws-sdk/eventstream-serde-browser": 3.226.0 - "@aws-sdk/eventstream-serde-config-resolver": 3.226.0 - "@aws-sdk/eventstream-serde-node": 3.226.0 - "@aws-sdk/fetch-http-handler": 3.226.0 - "@aws-sdk/hash-blob-browser": 3.226.0 - "@aws-sdk/hash-node": 3.226.0 - "@aws-sdk/hash-stream-node": 3.226.0 - "@aws-sdk/invalid-dependency": 3.226.0 - "@aws-sdk/md5-js": 3.226.0 - "@aws-sdk/middleware-bucket-endpoint": 3.226.0 - "@aws-sdk/middleware-content-length": 3.226.0 - "@aws-sdk/middleware-endpoint": 3.226.0 - "@aws-sdk/middleware-expect-continue": 3.226.0 - "@aws-sdk/middleware-flexible-checksums": 3.226.0 - "@aws-sdk/middleware-host-header": 3.226.0 - "@aws-sdk/middleware-location-constraint": 3.226.0 - "@aws-sdk/middleware-logger": 3.226.0 - "@aws-sdk/middleware-recursion-detection": 3.226.0 - "@aws-sdk/middleware-retry": 3.235.0 - "@aws-sdk/middleware-sdk-s3": 3.231.0 - "@aws-sdk/middleware-serde": 3.226.0 - "@aws-sdk/middleware-signing": 3.226.0 - "@aws-sdk/middleware-ssec": 3.226.0 - "@aws-sdk/middleware-stack": 3.226.0 - "@aws-sdk/middleware-user-agent": 3.226.0 - "@aws-sdk/node-config-provider": 3.226.0 - "@aws-sdk/node-http-handler": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/signature-v4-multi-region": 3.226.0 - "@aws-sdk/smithy-client": 3.234.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/url-parser": 3.226.0 + "@aws-crypto/sha1-browser": 3.0.0 + "@aws-crypto/sha256-browser": 3.0.0 + "@aws-crypto/sha256-js": 3.0.0 + "@aws-sdk/client-sts": 3.276.0 + "@aws-sdk/config-resolver": 3.272.0 + "@aws-sdk/credential-provider-node": 3.272.0 + "@aws-sdk/eventstream-serde-browser": 3.272.0 + "@aws-sdk/eventstream-serde-config-resolver": 3.272.0 + "@aws-sdk/eventstream-serde-node": 3.272.0 + "@aws-sdk/fetch-http-handler": 3.272.0 + "@aws-sdk/hash-blob-browser": 3.272.0 + "@aws-sdk/hash-node": 3.272.0 + "@aws-sdk/hash-stream-node": 3.272.0 + "@aws-sdk/invalid-dependency": 3.272.0 + "@aws-sdk/md5-js": 3.272.0 + "@aws-sdk/middleware-bucket-endpoint": 3.272.0 + "@aws-sdk/middleware-content-length": 3.272.0 + "@aws-sdk/middleware-endpoint": 3.272.0 + "@aws-sdk/middleware-expect-continue": 3.272.0 + "@aws-sdk/middleware-flexible-checksums": 3.272.0 + "@aws-sdk/middleware-host-header": 3.272.0 + "@aws-sdk/middleware-location-constraint": 3.272.0 + "@aws-sdk/middleware-logger": 3.272.0 + "@aws-sdk/middleware-recursion-detection": 3.272.0 + "@aws-sdk/middleware-retry": 3.272.0 + "@aws-sdk/middleware-sdk-s3": 3.272.0 + "@aws-sdk/middleware-serde": 3.272.0 + "@aws-sdk/middleware-signing": 3.272.0 + "@aws-sdk/middleware-ssec": 3.272.0 + "@aws-sdk/middleware-stack": 3.272.0 + "@aws-sdk/middleware-user-agent": 3.272.0 + "@aws-sdk/node-config-provider": 3.272.0 + "@aws-sdk/node-http-handler": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/signature-v4-multi-region": 3.272.0 + "@aws-sdk/smithy-client": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/url-parser": 3.272.0 "@aws-sdk/util-base64": 3.208.0 "@aws-sdk/util-body-length-browser": 3.188.0 "@aws-sdk/util-body-length-node": 3.208.0 - "@aws-sdk/util-defaults-mode-browser": 3.234.0 - "@aws-sdk/util-defaults-mode-node": 3.234.0 - "@aws-sdk/util-endpoints": 3.245.0 - "@aws-sdk/util-retry": 3.229.0 - "@aws-sdk/util-stream-browser": 3.226.0 - "@aws-sdk/util-stream-node": 3.226.0 - "@aws-sdk/util-user-agent-browser": 3.226.0 - "@aws-sdk/util-user-agent-node": 3.226.0 - "@aws-sdk/util-utf8-browser": 3.188.0 - "@aws-sdk/util-utf8-node": 3.208.0 - "@aws-sdk/util-waiter": 3.226.0 + "@aws-sdk/util-defaults-mode-browser": 3.272.0 + "@aws-sdk/util-defaults-mode-node": 3.272.0 + "@aws-sdk/util-endpoints": 3.272.0 + "@aws-sdk/util-retry": 3.272.0 + "@aws-sdk/util-stream-browser": 3.272.0 + "@aws-sdk/util-stream-node": 3.272.0 + "@aws-sdk/util-user-agent-browser": 3.272.0 + "@aws-sdk/util-user-agent-node": 3.272.0 + "@aws-sdk/util-utf8": 3.254.0 + "@aws-sdk/util-waiter": 3.272.0 "@aws-sdk/xml-builder": 3.201.0 - fast-xml-parser: 4.0.11 + fast-xml-parser: 4.1.2 tslib: ^2.3.1 - checksum: 7aaec8fdcfa1390222e6fc55d7314dbebab05c1afad8e580cd5137f71bb6cef7ffaca113b117af22ee2ac110b03fbf7eec173f16cf093b0e1366f218169db7a4 + checksum: f7f475b92c0cff800536fb910bb6ffefcee6e650208f1791ea64c95bcbb6c63ef7a55e3f72af44e2e730a53865857ac4b974f00cb7da1e369d742cc95e33f02a languageName: node linkType: hard "@aws-sdk/client-sqs@npm:^3.208.0": - version: 3.245.0 - resolution: "@aws-sdk/client-sqs@npm:3.245.0" + version: 3.276.0 + resolution: "@aws-sdk/client-sqs@npm:3.276.0" dependencies: - "@aws-crypto/sha256-browser": 2.0.0 - "@aws-crypto/sha256-js": 2.0.0 - "@aws-sdk/client-sts": 3.245.0 - "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/credential-provider-node": 3.245.0 - "@aws-sdk/fetch-http-handler": 3.226.0 - "@aws-sdk/hash-node": 3.226.0 - "@aws-sdk/invalid-dependency": 3.226.0 - "@aws-sdk/md5-js": 3.226.0 - "@aws-sdk/middleware-content-length": 3.226.0 - "@aws-sdk/middleware-endpoint": 3.226.0 - "@aws-sdk/middleware-host-header": 3.226.0 - "@aws-sdk/middleware-logger": 3.226.0 - "@aws-sdk/middleware-recursion-detection": 3.226.0 - "@aws-sdk/middleware-retry": 3.235.0 - "@aws-sdk/middleware-sdk-sqs": 3.226.0 - "@aws-sdk/middleware-serde": 3.226.0 - "@aws-sdk/middleware-signing": 3.226.0 - "@aws-sdk/middleware-stack": 3.226.0 - "@aws-sdk/middleware-user-agent": 3.226.0 - "@aws-sdk/node-config-provider": 3.226.0 - "@aws-sdk/node-http-handler": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/smithy-client": 3.234.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/url-parser": 3.226.0 + "@aws-crypto/sha256-browser": 3.0.0 + "@aws-crypto/sha256-js": 3.0.0 + "@aws-sdk/client-sts": 3.276.0 + "@aws-sdk/config-resolver": 3.272.0 + "@aws-sdk/credential-provider-node": 3.272.0 + "@aws-sdk/fetch-http-handler": 3.272.0 + "@aws-sdk/hash-node": 3.272.0 + "@aws-sdk/invalid-dependency": 3.272.0 + "@aws-sdk/md5-js": 3.272.0 + "@aws-sdk/middleware-content-length": 3.272.0 + "@aws-sdk/middleware-endpoint": 3.272.0 + "@aws-sdk/middleware-host-header": 3.272.0 + "@aws-sdk/middleware-logger": 3.272.0 + "@aws-sdk/middleware-recursion-detection": 3.272.0 + "@aws-sdk/middleware-retry": 3.272.0 + "@aws-sdk/middleware-sdk-sqs": 3.272.0 + "@aws-sdk/middleware-serde": 3.272.0 + "@aws-sdk/middleware-signing": 3.272.0 + "@aws-sdk/middleware-stack": 3.272.0 + "@aws-sdk/middleware-user-agent": 3.272.0 + "@aws-sdk/node-config-provider": 3.272.0 + "@aws-sdk/node-http-handler": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/smithy-client": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/url-parser": 3.272.0 "@aws-sdk/util-base64": 3.208.0 "@aws-sdk/util-body-length-browser": 3.188.0 "@aws-sdk/util-body-length-node": 3.208.0 - "@aws-sdk/util-defaults-mode-browser": 3.234.0 - "@aws-sdk/util-defaults-mode-node": 3.234.0 - "@aws-sdk/util-endpoints": 3.245.0 - "@aws-sdk/util-retry": 3.229.0 - "@aws-sdk/util-user-agent-browser": 3.226.0 - "@aws-sdk/util-user-agent-node": 3.226.0 - "@aws-sdk/util-utf8-browser": 3.188.0 - "@aws-sdk/util-utf8-node": 3.208.0 - fast-xml-parser: 4.0.11 + "@aws-sdk/util-defaults-mode-browser": 3.272.0 + "@aws-sdk/util-defaults-mode-node": 3.272.0 + "@aws-sdk/util-endpoints": 3.272.0 + "@aws-sdk/util-retry": 3.272.0 + "@aws-sdk/util-user-agent-browser": 3.272.0 + "@aws-sdk/util-user-agent-node": 3.272.0 + "@aws-sdk/util-utf8": 3.254.0 + fast-xml-parser: 4.1.2 tslib: ^2.3.1 - checksum: 994c18f001763e63b8cfd86ddcb8c7c8460f8bb0507759037315d15a37e2a2e49833091ec6db7b4181323a389b3b14085cf87eb00c1980479f1e184d63d7de74 + checksum: 88139fde34fa6348e841de13fce5e68b2dfb735c5358c959f9956a8c918048bde8b1ff701e8e701807af5f67f6df129056986b62a7e07096cc7097f3491d36b8 languageName: node linkType: hard -"@aws-sdk/client-sso-oidc@npm:3.245.0": - version: 3.245.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.245.0" +"@aws-sdk/client-sso-oidc@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.272.0" dependencies: - "@aws-crypto/sha256-browser": 2.0.0 - "@aws-crypto/sha256-js": 2.0.0 - "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/fetch-http-handler": 3.226.0 - "@aws-sdk/hash-node": 3.226.0 - "@aws-sdk/invalid-dependency": 3.226.0 - "@aws-sdk/middleware-content-length": 3.226.0 - "@aws-sdk/middleware-endpoint": 3.226.0 - "@aws-sdk/middleware-host-header": 3.226.0 - "@aws-sdk/middleware-logger": 3.226.0 - "@aws-sdk/middleware-recursion-detection": 3.226.0 - "@aws-sdk/middleware-retry": 3.235.0 - "@aws-sdk/middleware-serde": 3.226.0 - "@aws-sdk/middleware-stack": 3.226.0 - "@aws-sdk/middleware-user-agent": 3.226.0 - "@aws-sdk/node-config-provider": 3.226.0 - "@aws-sdk/node-http-handler": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/smithy-client": 3.234.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/url-parser": 3.226.0 + "@aws-crypto/sha256-browser": 3.0.0 + "@aws-crypto/sha256-js": 3.0.0 + "@aws-sdk/config-resolver": 3.272.0 + "@aws-sdk/fetch-http-handler": 3.272.0 + "@aws-sdk/hash-node": 3.272.0 + "@aws-sdk/invalid-dependency": 3.272.0 + "@aws-sdk/middleware-content-length": 3.272.0 + "@aws-sdk/middleware-endpoint": 3.272.0 + "@aws-sdk/middleware-host-header": 3.272.0 + "@aws-sdk/middleware-logger": 3.272.0 + "@aws-sdk/middleware-recursion-detection": 3.272.0 + "@aws-sdk/middleware-retry": 3.272.0 + "@aws-sdk/middleware-serde": 3.272.0 + "@aws-sdk/middleware-stack": 3.272.0 + "@aws-sdk/middleware-user-agent": 3.272.0 + "@aws-sdk/node-config-provider": 3.272.0 + "@aws-sdk/node-http-handler": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/smithy-client": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/url-parser": 3.272.0 "@aws-sdk/util-base64": 3.208.0 "@aws-sdk/util-body-length-browser": 3.188.0 "@aws-sdk/util-body-length-node": 3.208.0 - "@aws-sdk/util-defaults-mode-browser": 3.234.0 - "@aws-sdk/util-defaults-mode-node": 3.234.0 - "@aws-sdk/util-endpoints": 3.245.0 - "@aws-sdk/util-retry": 3.229.0 - "@aws-sdk/util-user-agent-browser": 3.226.0 - "@aws-sdk/util-user-agent-node": 3.226.0 - "@aws-sdk/util-utf8-browser": 3.188.0 - "@aws-sdk/util-utf8-node": 3.208.0 + "@aws-sdk/util-defaults-mode-browser": 3.272.0 + "@aws-sdk/util-defaults-mode-node": 3.272.0 + "@aws-sdk/util-endpoints": 3.272.0 + "@aws-sdk/util-retry": 3.272.0 + "@aws-sdk/util-user-agent-browser": 3.272.0 + "@aws-sdk/util-user-agent-node": 3.272.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: 6f72ee0df276f8c4019be4d9b3fa1b2cbab7744b13f434f07c9314303290334296be1148276266715ef3cfb37e83ced448fd9d6bb34d09bd21ed41d0a83598dc + checksum: a1ee61c2fc86a74439e06c98aa10c8246e31579eb480b625321f7dfe8fd26af49bd6ead803da2443700c5943eab5ba90dfc33fc4d9f23b1a680850d73526da4d languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.245.0": - version: 3.245.0 - resolution: "@aws-sdk/client-sso@npm:3.245.0" +"@aws-sdk/client-sso@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/client-sso@npm:3.272.0" dependencies: - "@aws-crypto/sha256-browser": 2.0.0 - "@aws-crypto/sha256-js": 2.0.0 - "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/fetch-http-handler": 3.226.0 - "@aws-sdk/hash-node": 3.226.0 - "@aws-sdk/invalid-dependency": 3.226.0 - "@aws-sdk/middleware-content-length": 3.226.0 - "@aws-sdk/middleware-endpoint": 3.226.0 - "@aws-sdk/middleware-host-header": 3.226.0 - "@aws-sdk/middleware-logger": 3.226.0 - "@aws-sdk/middleware-recursion-detection": 3.226.0 - "@aws-sdk/middleware-retry": 3.235.0 - "@aws-sdk/middleware-serde": 3.226.0 - "@aws-sdk/middleware-stack": 3.226.0 - "@aws-sdk/middleware-user-agent": 3.226.0 - "@aws-sdk/node-config-provider": 3.226.0 - "@aws-sdk/node-http-handler": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/smithy-client": 3.234.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/url-parser": 3.226.0 + "@aws-crypto/sha256-browser": 3.0.0 + "@aws-crypto/sha256-js": 3.0.0 + "@aws-sdk/config-resolver": 3.272.0 + "@aws-sdk/fetch-http-handler": 3.272.0 + "@aws-sdk/hash-node": 3.272.0 + "@aws-sdk/invalid-dependency": 3.272.0 + "@aws-sdk/middleware-content-length": 3.272.0 + "@aws-sdk/middleware-endpoint": 3.272.0 + "@aws-sdk/middleware-host-header": 3.272.0 + "@aws-sdk/middleware-logger": 3.272.0 + "@aws-sdk/middleware-recursion-detection": 3.272.0 + "@aws-sdk/middleware-retry": 3.272.0 + "@aws-sdk/middleware-serde": 3.272.0 + "@aws-sdk/middleware-stack": 3.272.0 + "@aws-sdk/middleware-user-agent": 3.272.0 + "@aws-sdk/node-config-provider": 3.272.0 + "@aws-sdk/node-http-handler": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/smithy-client": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/url-parser": 3.272.0 "@aws-sdk/util-base64": 3.208.0 "@aws-sdk/util-body-length-browser": 3.188.0 "@aws-sdk/util-body-length-node": 3.208.0 - "@aws-sdk/util-defaults-mode-browser": 3.234.0 - "@aws-sdk/util-defaults-mode-node": 3.234.0 - "@aws-sdk/util-endpoints": 3.245.0 - "@aws-sdk/util-retry": 3.229.0 - "@aws-sdk/util-user-agent-browser": 3.226.0 - "@aws-sdk/util-user-agent-node": 3.226.0 - "@aws-sdk/util-utf8-browser": 3.188.0 - "@aws-sdk/util-utf8-node": 3.208.0 + "@aws-sdk/util-defaults-mode-browser": 3.272.0 + "@aws-sdk/util-defaults-mode-node": 3.272.0 + "@aws-sdk/util-endpoints": 3.272.0 + "@aws-sdk/util-retry": 3.272.0 + "@aws-sdk/util-user-agent-browser": 3.272.0 + "@aws-sdk/util-user-agent-node": 3.272.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: e7dd235ee13465438a181c5de2f5bfc1acdaa12a07db74a3255dbb580004c33544d6773252b164dda2e2357ea83f3985afd060dfe42d6064732d36750764eda1 + checksum: 7549e813ef8088d22a0a1f9c8a42532d2d2050c932fd6e7d4f717aea2cd554938283f3af0f1fe9597b24724c6b2438947b3089452ed9c0dfafb1efad15f3ac60 languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.245.0, @aws-sdk/client-sts@npm:^3.208.0": - version: 3.245.0 - resolution: "@aws-sdk/client-sts@npm:3.245.0" +"@aws-sdk/client-sts@npm:3.276.0, @aws-sdk/client-sts@npm:^3.208.0": + version: 3.276.0 + resolution: "@aws-sdk/client-sts@npm:3.276.0" dependencies: - "@aws-crypto/sha256-browser": 2.0.0 - "@aws-crypto/sha256-js": 2.0.0 - "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/credential-provider-node": 3.245.0 - "@aws-sdk/fetch-http-handler": 3.226.0 - "@aws-sdk/hash-node": 3.226.0 - "@aws-sdk/invalid-dependency": 3.226.0 - "@aws-sdk/middleware-content-length": 3.226.0 - "@aws-sdk/middleware-endpoint": 3.226.0 - "@aws-sdk/middleware-host-header": 3.226.0 - "@aws-sdk/middleware-logger": 3.226.0 - "@aws-sdk/middleware-recursion-detection": 3.226.0 - "@aws-sdk/middleware-retry": 3.235.0 - "@aws-sdk/middleware-sdk-sts": 3.226.0 - "@aws-sdk/middleware-serde": 3.226.0 - "@aws-sdk/middleware-signing": 3.226.0 - "@aws-sdk/middleware-stack": 3.226.0 - "@aws-sdk/middleware-user-agent": 3.226.0 - "@aws-sdk/node-config-provider": 3.226.0 - "@aws-sdk/node-http-handler": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/smithy-client": 3.234.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/url-parser": 3.226.0 + "@aws-crypto/sha256-browser": 3.0.0 + "@aws-crypto/sha256-js": 3.0.0 + "@aws-sdk/config-resolver": 3.272.0 + "@aws-sdk/credential-provider-node": 3.272.0 + "@aws-sdk/fetch-http-handler": 3.272.0 + "@aws-sdk/hash-node": 3.272.0 + "@aws-sdk/invalid-dependency": 3.272.0 + "@aws-sdk/middleware-content-length": 3.272.0 + "@aws-sdk/middleware-endpoint": 3.272.0 + "@aws-sdk/middleware-host-header": 3.272.0 + "@aws-sdk/middleware-logger": 3.272.0 + "@aws-sdk/middleware-recursion-detection": 3.272.0 + "@aws-sdk/middleware-retry": 3.272.0 + "@aws-sdk/middleware-sdk-sts": 3.272.0 + "@aws-sdk/middleware-serde": 3.272.0 + "@aws-sdk/middleware-signing": 3.272.0 + "@aws-sdk/middleware-stack": 3.272.0 + "@aws-sdk/middleware-user-agent": 3.272.0 + "@aws-sdk/node-config-provider": 3.272.0 + "@aws-sdk/node-http-handler": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/smithy-client": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/url-parser": 3.272.0 "@aws-sdk/util-base64": 3.208.0 "@aws-sdk/util-body-length-browser": 3.188.0 "@aws-sdk/util-body-length-node": 3.208.0 - "@aws-sdk/util-defaults-mode-browser": 3.234.0 - "@aws-sdk/util-defaults-mode-node": 3.234.0 - "@aws-sdk/util-endpoints": 3.245.0 - "@aws-sdk/util-retry": 3.229.0 - "@aws-sdk/util-user-agent-browser": 3.226.0 - "@aws-sdk/util-user-agent-node": 3.226.0 - "@aws-sdk/util-utf8-browser": 3.188.0 - "@aws-sdk/util-utf8-node": 3.208.0 - fast-xml-parser: 4.0.11 + "@aws-sdk/util-defaults-mode-browser": 3.272.0 + "@aws-sdk/util-defaults-mode-node": 3.272.0 + "@aws-sdk/util-endpoints": 3.272.0 + "@aws-sdk/util-retry": 3.272.0 + "@aws-sdk/util-user-agent-browser": 3.272.0 + "@aws-sdk/util-user-agent-node": 3.272.0 + "@aws-sdk/util-utf8": 3.254.0 + fast-xml-parser: 4.1.2 tslib: ^2.3.1 - checksum: 3b052be86063978b47b2f4459062ea23a6d94c81951a97428a5445ff062d642a3460114cbf1b20d3925db0667d893db40cbe30b1b8b58a4d749c75d117aab460 + checksum: 3250322448500789588d24b6fac295b551ab4c1661ab9aa93f173e63b4673260bb2e551c079d811da752115e583b76f5c7ffb16d94b3c66275e75e52a93f35f3 languageName: node linkType: hard -"@aws-sdk/config-resolver@npm:3.234.0": - version: 3.234.0 - resolution: "@aws-sdk/config-resolver@npm:3.234.0" +"@aws-sdk/config-resolver@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/config-resolver@npm:3.272.0" dependencies: - "@aws-sdk/signature-v4": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/signature-v4": 3.272.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-config-provider": 3.208.0 - "@aws-sdk/util-middleware": 3.226.0 + "@aws-sdk/util-middleware": 3.272.0 tslib: ^2.3.1 - checksum: a72bdb1748d7f04c95436ab1e8650b51c1bfd2745114e52f7d05339c61bb6a859306be0c6cc0034ecf4f3b29a2d68b48fe362762a58c90ec55a3444b93b5700d + checksum: d2a2fa4f013ad04f50522aec1001efd34c62ababbc809b176d2e31e41828d867de616ff01bfcaa12603b344ca6838ea5946c72d21e5f2573b90828e29bad6e35 languageName: node linkType: hard -"@aws-sdk/credential-provider-cognito-identity@npm:3.245.0": - version: 3.245.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.245.0" +"@aws-sdk/credential-provider-cognito-identity@npm:3.276.0": + version: 3.276.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.276.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.245.0 - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/client-cognito-identity": 3.276.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: af9ce2a19426056139c380fa2a016d5abb42117e262dc5c3f4741eb1f2d15b394e9b178d6efb2e32f59cd723c6090da769975505368fbbe79b8eb9258c6961a8 + checksum: 2d255475f08beeccd000745b2f2c46553559fd6fe15c0ea9960956655535fb927df9ec3431cf743b80f9a460d033ede7e1c47e91aff44956559a70c5f43f7bab languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.226.0" +"@aws-sdk/credential-provider-env@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.272.0" dependencies: - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 4f764d8d07ba1f65503ed2440ebfbccf98c141b11a087a582ea606d06bc764c91641b4ec505ab0d4750025a01128b041d9514a3ec868c385f038b5373b513293 + checksum: 74195ecb608e3e6dedfc40e052bd7d0ef3a9e531ecf404f40d05869e23328bd156e458e9770d30458137438de92efd3dcb9fc3ff5f1319c49b55e057323f3ea6 languageName: node linkType: hard -"@aws-sdk/credential-provider-imds@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/credential-provider-imds@npm:3.226.0" +"@aws-sdk/credential-provider-imds@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/credential-provider-imds@npm:3.272.0" dependencies: - "@aws-sdk/node-config-provider": 3.226.0 - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/url-parser": 3.226.0 + "@aws-sdk/node-config-provider": 3.272.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/url-parser": 3.272.0 tslib: ^2.3.1 - checksum: c0480ca127e6715ebf98ff41a8ea47d139166b8632582782df46fc9e568ff579cfc0768af80082ae6eea3eb6750edb41c4a4c39281b777c578fdb7f8ef8b641c + checksum: c6adfcaa8dada3f99bbbd241aee3e568474c93df97e9b3f5e4feb9b000a91e5b3f0491556f712ebbd4d67de9c77c1072e6d444ac63ad9c6be31cf26acb2bc72c languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.245.0": - version: 3.245.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.245.0" +"@aws-sdk/credential-provider-ini@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.272.0" dependencies: - "@aws-sdk/credential-provider-env": 3.226.0 - "@aws-sdk/credential-provider-imds": 3.226.0 - "@aws-sdk/credential-provider-process": 3.226.0 - "@aws-sdk/credential-provider-sso": 3.245.0 - "@aws-sdk/credential-provider-web-identity": 3.226.0 - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/shared-ini-file-loader": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/credential-provider-env": 3.272.0 + "@aws-sdk/credential-provider-imds": 3.272.0 + "@aws-sdk/credential-provider-process": 3.272.0 + "@aws-sdk/credential-provider-sso": 3.272.0 + "@aws-sdk/credential-provider-web-identity": 3.272.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/shared-ini-file-loader": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 35fcdb231efb573c60c25e67a27f64ca892ebab7931e715406826a4d3f87caecd525945f45ecaf7fb7e09a87a744678aba60249c74681160c9fb9d66c0490bf5 + checksum: 06c1dfc6bff09472e11dd7e2c295cfdf71c36af11cb6ba33ce0328aaf9d0baee3f99de8ceec32ff473925b55ad190c9f85f125cf43874c25e22c37e8e3c648d3 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.245.0, @aws-sdk/credential-provider-node@npm:^3.208.0": - version: 3.245.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.245.0" +"@aws-sdk/credential-provider-node@npm:3.272.0, @aws-sdk/credential-provider-node@npm:^3.208.0": + version: 3.272.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.272.0" dependencies: - "@aws-sdk/credential-provider-env": 3.226.0 - "@aws-sdk/credential-provider-imds": 3.226.0 - "@aws-sdk/credential-provider-ini": 3.245.0 - "@aws-sdk/credential-provider-process": 3.226.0 - "@aws-sdk/credential-provider-sso": 3.245.0 - "@aws-sdk/credential-provider-web-identity": 3.226.0 - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/shared-ini-file-loader": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/credential-provider-env": 3.272.0 + "@aws-sdk/credential-provider-imds": 3.272.0 + "@aws-sdk/credential-provider-ini": 3.272.0 + "@aws-sdk/credential-provider-process": 3.272.0 + "@aws-sdk/credential-provider-sso": 3.272.0 + "@aws-sdk/credential-provider-web-identity": 3.272.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/shared-ini-file-loader": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: a9bd115016fc7ce7f2cb50c5cafcd759f861362bb9dd9629e4ea0056519d5b26ad256f6d1d84ef3327c75bef442de8c0e4b54b215870512b43c92866ed0f1403 + checksum: 25c5ab8583821f63c7eb1a5257869dd21159d0b7643ff9a9c094e9b5c78c887bc441f1a2f974eaace62c1879d13e7d590bb3ea2623f5a5de822bf773b1aab100 languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.226.0" +"@aws-sdk/credential-provider-process@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.272.0" dependencies: - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/shared-ini-file-loader": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/shared-ini-file-loader": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 3ad24e2784ae7929f8932a187e0a5a06fc62d31ccb393fd2d9fa86d75c1109809c96b69edb17c3d7bd7d087b97662818d5320742a4cd70add06999a3b2ec3a53 + checksum: fda290aea2417137f333c8ad0a94d7789054b464bdf5f9a7eef06341f88d4bd0c96ca02b84b58e0af2a9c490cee4febf6ee84254dfbff192bfccccbb0c8c1876 languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.245.0": - version: 3.245.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.245.0" +"@aws-sdk/credential-provider-sso@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.272.0" dependencies: - "@aws-sdk/client-sso": 3.245.0 - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/shared-ini-file-loader": 3.226.0 - "@aws-sdk/token-providers": 3.245.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/client-sso": 3.272.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/shared-ini-file-loader": 3.272.0 + "@aws-sdk/token-providers": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 95358504631d5451520d5090e7fb337b40dfc12ab30dd84d979165d881f2762798828003b13adcb35921223bf3a498b0e70b7a94ebc14e8b16a4f0adacded8f1 + checksum: ac1ada61e67f6f0a82ed8507c0218a03fe40e2d1e4ee064578e63573f868195800088dc3e33d58d7d04877e464342987005aceb300e4db9a141080cdc695e46e languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.226.0" +"@aws-sdk/credential-provider-web-identity@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.272.0" dependencies: - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: f536d9af3d900eee31b6e7966ff827713fd5f661ae477f0911eebdac3e544eb5a732df0be0c9162058943e0aeb89333c97a1758236e8f9760c5e87280fd926fa + checksum: aad84e2bfd90495ed4eb76b83940729bd2fb652398f7e477cfd8053f9c986132b4ff8682854a91e3f23c7daaab66e1c31320188561104fea710cd87be1221e9d languageName: node linkType: hard "@aws-sdk/credential-providers@npm:^3.208.0": - version: 3.245.0 - resolution: "@aws-sdk/credential-providers@npm:3.245.0" + version: 3.276.0 + resolution: "@aws-sdk/credential-providers@npm:3.276.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.245.0 - "@aws-sdk/client-sso": 3.245.0 - "@aws-sdk/client-sts": 3.245.0 - "@aws-sdk/credential-provider-cognito-identity": 3.245.0 - "@aws-sdk/credential-provider-env": 3.226.0 - "@aws-sdk/credential-provider-imds": 3.226.0 - "@aws-sdk/credential-provider-ini": 3.245.0 - "@aws-sdk/credential-provider-node": 3.245.0 - "@aws-sdk/credential-provider-process": 3.226.0 - "@aws-sdk/credential-provider-sso": 3.245.0 - "@aws-sdk/credential-provider-web-identity": 3.226.0 - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/shared-ini-file-loader": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/client-cognito-identity": 3.276.0 + "@aws-sdk/client-sso": 3.272.0 + "@aws-sdk/client-sts": 3.276.0 + "@aws-sdk/credential-provider-cognito-identity": 3.276.0 + "@aws-sdk/credential-provider-env": 3.272.0 + "@aws-sdk/credential-provider-imds": 3.272.0 + "@aws-sdk/credential-provider-ini": 3.272.0 + "@aws-sdk/credential-provider-node": 3.272.0 + "@aws-sdk/credential-provider-process": 3.272.0 + "@aws-sdk/credential-provider-sso": 3.272.0 + "@aws-sdk/credential-provider-web-identity": 3.272.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/shared-ini-file-loader": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: a6923907ce97232cf87565e5fc0dac872061d2f065f530f7c91d1693472dd26667a91a662283994b3886a84b557f45e8627d0c847a88ffa0fdf2458a0180bd43 + checksum: 825fb0feda57ef98cc11ab3febc2a337161193dfa24bcc31ad2b47ecb4af564b3b5a5a0d9c17fd1a3ec118321d21fc515948980ba50c323e21ed79905c13dfe4 languageName: node linkType: hard -"@aws-sdk/eventstream-codec@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/eventstream-codec@npm:3.226.0" +"@aws-sdk/eventstream-codec@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/eventstream-codec@npm:3.272.0" dependencies: - "@aws-crypto/crc32": 2.0.0 - "@aws-sdk/types": 3.226.0 + "@aws-crypto/crc32": 3.0.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-hex-encoding": 3.201.0 tslib: ^2.3.1 - checksum: 3b3a584c02bcf6cfe35e2ed562e8d82844a51c1efd2f9d79a72ecf60ba9a63a51c568c8b5a49cdfb6c49fe655be0454c082098285378c66494109dbf7b5bb28f + checksum: bea8599a72657d698de169643950031624e0b61179240c8d8eb3c17f01bfc4579c507a861be6c703f318fcdff4217a4cb4279223cad93fb21e69f4b5d10d6102 languageName: node linkType: hard -"@aws-sdk/eventstream-serde-browser@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/eventstream-serde-browser@npm:3.226.0" +"@aws-sdk/eventstream-serde-browser@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/eventstream-serde-browser@npm:3.272.0" dependencies: - "@aws-sdk/eventstream-serde-universal": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/eventstream-serde-universal": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 12530898d3debe33fbc73c6b68c7a889e4c3e29c183180d9d012e9b3761af1fd58feaa9a6e8069ccf380442c7d71ad6f36d5f0af078ae6d29af944ca6f4a9610 + checksum: a4fa42e1a8658eca191f567046741192e6e04f5c58744655ec0112a1d46b57d5ce04432e2fec0e6fe16d486c30abdc58b734ed67879c10e0fcc4820530eaff35 languageName: node linkType: hard -"@aws-sdk/eventstream-serde-config-resolver@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/eventstream-serde-config-resolver@npm:3.226.0" +"@aws-sdk/eventstream-serde-config-resolver@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/eventstream-serde-config-resolver@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 2438bfd3bf00e401caa79c2b874be6ecfac24db74f3ec9e1064a3d5c09a7cfe0fa9c0086e30ee3bc8853f550efc6c6ecb2936caafd238478870a803863fbea23 + checksum: feee058288bbf5a97d78dfee5084c2dc224d771499848dcaedbfd671c8889f52329f96ce87c291d29e66b46c8e25923bd55f64d8ed744089fc40723296eb003c languageName: node linkType: hard -"@aws-sdk/eventstream-serde-node@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/eventstream-serde-node@npm:3.226.0" +"@aws-sdk/eventstream-serde-node@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/eventstream-serde-node@npm:3.272.0" dependencies: - "@aws-sdk/eventstream-serde-universal": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/eventstream-serde-universal": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 89c2154fb4bb544f87e5fdda37cc99f724125892417d78763a98a0a4390b2ba89e8a57521a57b213827688ce8d8bc14971ae87f1137cc26d9f3ebbae142d5a96 + checksum: 02fa0c5f992df253713bbe19515bf898e332e3bc51da28da28bdf2cde584df90689bb9b54afa57354798c8b01e4b592d63fb575d7652fd5df2c00bf1e45e689a languageName: node linkType: hard -"@aws-sdk/eventstream-serde-universal@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/eventstream-serde-universal@npm:3.226.0" +"@aws-sdk/eventstream-serde-universal@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/eventstream-serde-universal@npm:3.272.0" dependencies: - "@aws-sdk/eventstream-codec": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/eventstream-codec": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: da1eea2d8b0f6972a5016ca66b2a8da0e2e72b8449a661c8cf4678894e22bcef20d5c1534c5c5a22b4c7d1d242bf894b7220c8d712123e092efa8c5fa8aca02f + checksum: 3d8031e84af1a0cdf5f22f98aee868cd62b6fa87e432231bbe32a400874dc44b030cffdf9dce3194281bf04a1669b23bf8ce16759123ba10980676fe9388e63b languageName: node linkType: hard @@ -902,59 +889,61 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/fetch-http-handler@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/fetch-http-handler@npm:3.226.0" +"@aws-sdk/fetch-http-handler@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/fetch-http-handler@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/querystring-builder": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/querystring-builder": 3.272.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-base64": 3.208.0 tslib: ^2.3.1 - checksum: f12ed12088aee05e4c6a742356017f55f26fec740c12c4d89c4eb283203171d032d99c9fa45fb28cc7798b35410cc55736df1c78d8eea014667e489f1b276f3a + checksum: 63d047db7b7ebfee49af8bf42f3a7aa1bea37cc687e7046bc77913c23ed99056e91b5ab77739157428be73b2a2f29783a7a659ccf29a440dd63b750d313d46cc languageName: node linkType: hard -"@aws-sdk/hash-blob-browser@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/hash-blob-browser@npm:3.226.0" +"@aws-sdk/hash-blob-browser@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/hash-blob-browser@npm:3.272.0" dependencies: "@aws-sdk/chunked-blob-reader": 3.188.0 "@aws-sdk/chunked-blob-reader-native": 3.208.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 98fecc0646a37bfb541a2e8bd1da27f0b208f9d51e6efc0e2e9e833cb1872a4205c5b47568582805a3e98240f3fe2f16a5aaee6693d1e1589fd6918afce554b0 + checksum: ab0564672606760b934973ef33a8a8d567d52d3fa4be2a7a8919daa1b9ac1c3afaaee975ae7ad20940f51e1057e3321c39f5dcd2246ca85aad03e1f7aed68669 languageName: node linkType: hard -"@aws-sdk/hash-node@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/hash-node@npm:3.226.0" +"@aws-sdk/hash-node@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/hash-node@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-buffer-from": 3.208.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: cbe466e2e3efdbb3aefe7772da734f6d00e0d1572c60e497b09460e055093b155c5e94bd3718135238bc05e58cb49abcdcd0fe5e34458bd3563b89d3d4a42251 + checksum: 1b3d5b288133af9294df36cec2377589f45c82f61cb824b14ac38955541357826cf5a8f95d7906cbf284ed9ffbd928a37b2f94581f7281715879aa4c0e7c7b24 languageName: node linkType: hard -"@aws-sdk/hash-stream-node@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/hash-stream-node@npm:3.226.0" +"@aws-sdk/hash-stream-node@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/hash-stream-node@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: a6acb592e815fa48b39e7917599405ebf77c8c03e42ef039eea8886e0bda98c4d8a963506b137514787dca65cf4594cf756d49adcc67b0777b71f2ce7c4cd0da + checksum: 4cb90a9ae442e0722d651d7e7558a8b0abcbc1b45670e194d51d0a9b2ea6cad6e3a02433d506a8d96ac94a26cd8e5746514c09f05f0da2acb2dc06c60218d3c5 languageName: node linkType: hard -"@aws-sdk/invalid-dependency@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/invalid-dependency@npm:3.226.0" +"@aws-sdk/invalid-dependency@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/invalid-dependency@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 4a6168b50675881442c9bc87e8f27e8317ece9251fbfd3d812d32edcd83f687328e6de518fa9f11b1bc984ad3a3454cd43b59c0362f04c03687ce5250ac5d720 + checksum: 4ef63ed9a9275e20bdcec551ee92a9d60a6a688c120f968fd51af235d357d39ec522752592075dad752be9a8439d93b4d1848e12fa9a88cda595920d78b008d2 languageName: node linkType: hard @@ -968,11 +957,11 @@ __metadata: linkType: hard "@aws-sdk/lib-storage@npm:^3.208.0": - version: 3.245.0 - resolution: "@aws-sdk/lib-storage@npm:3.245.0" + version: 3.276.0 + resolution: "@aws-sdk/lib-storage@npm:3.276.0" dependencies: - "@aws-sdk/middleware-endpoint": 3.226.0 - "@aws-sdk/smithy-client": 3.234.0 + "@aws-sdk/middleware-endpoint": 3.272.0 + "@aws-sdk/smithy-client": 3.272.0 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 @@ -980,267 +969,268 @@ __metadata: peerDependencies: "@aws-sdk/abort-controller": ^3.0.0 "@aws-sdk/client-s3": ^3.0.0 - checksum: 580221e5f577aad98a8b2ea362ec1c8198e8766ddc1503c4a0bb4afd2c96ad09b6cfc2d5aa252c7d0d3d8215a55bb39c65df1aceb525fb4f1118b4a1b801818d + checksum: 1e34d3569c8cf2f7c42efdd7966a9faea64bf49f4fe1fcb0ff30ed86670a5ba2361bb14279e7dc1bc110a1ff9e5cec3551bc8a56b3c9c4d19f98ff4842a8de19 languageName: node linkType: hard -"@aws-sdk/md5-js@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/md5-js@npm:3.226.0" +"@aws-sdk/md5-js@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/md5-js@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 - "@aws-sdk/util-utf8-browser": 3.188.0 - "@aws-sdk/util-utf8-node": 3.208.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: 7d2dc4fefaf3136bf30e22f4645588624c8e2696b638357676f18df3ea1c11a7fd8dddc5b50cae739383590b2d3e10be02fd033c764c57c882b5a2230edb2d6f + checksum: d59010c1b48ba1992298ed23b605e7395a4aa964430d41012492faac24c7fa50856b565a523b0262b2c8b667904fdc57748d98475ba8c2e24b4550e17211e5ab languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.226.0" +"@aws-sdk/middleware-bucket-endpoint@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-arn-parser": 3.208.0 "@aws-sdk/util-config-provider": 3.208.0 tslib: ^2.3.1 - checksum: bb5e4a8bd79d578c103d216274a8175c42cea06c138338fa0854a30b8eab0c6a14d5996814f2ac9dcc82e1f137a240e657bb1e8841b52d8ade5c275771c43ca5 + checksum: 2c9dffb5545e085ce6323e9628d5998b551332f213f33cdbd606e889b4ca4fdac61648e2c0d8cadcf11aa5908049a764083a794082fce611980056a093c78e55 languageName: node linkType: hard -"@aws-sdk/middleware-content-length@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-content-length@npm:3.226.0" +"@aws-sdk/middleware-content-length@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-content-length@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 538b0222e2c8be61dc3090dfefe4b9ec1f0cbee155125edf5b654dbe36ab0b2d6befe3da6008194dc26bbf94d53b7e01edca683720061c4b43c6beb9dc278002 + checksum: e1a9c3e6fec8dcfed90bb44b25a8f4786b4ed0cbd79c6f69e3d8e91d394402ad4f2667231aafdcc05caf136a331434d54fac286a72ab5870dd90705a2e56243b languageName: node linkType: hard -"@aws-sdk/middleware-endpoint@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-endpoint@npm:3.226.0" +"@aws-sdk/middleware-endpoint@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-endpoint@npm:3.272.0" dependencies: - "@aws-sdk/middleware-serde": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/signature-v4": 3.226.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/url-parser": 3.226.0 + "@aws-sdk/middleware-serde": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/signature-v4": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/url-parser": 3.272.0 "@aws-sdk/util-config-provider": 3.208.0 - "@aws-sdk/util-middleware": 3.226.0 + "@aws-sdk/util-middleware": 3.272.0 tslib: ^2.3.1 - checksum: 22df6fd90e3b7d3edd58f427ebfbb229181ef190b2d795e0b400f70f95a573dc59666a08b26613db6bb1b764b13ba7ddec15fced2641e9678fad5500fca289be + checksum: c3acd1a35d33cff87a43df371ae138faf6c9569497c36b009cafc2fdb1f21e352671df6eb56730634d7f135bef2a47ac588ac7cdd2905676292a418b79a0eab1 languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.226.0" +"@aws-sdk/middleware-expect-continue@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 22924f008a7594cedde2c1f09b9b8ce005cf73bfcbf40330a2b783e42dffcd1fc88fd3a7b2b13da1ec4895c649f7605acea7b62dbbd3a45b00630b1cb9579bc4 + checksum: 19f32623758ccc1d08e2ff08db8efc62c3e04c3e89b15a7ca4b1719fa15ce7bfaad0eea3cceadc395f769405181a356a6a4efdadb2e6f6bd90afd31f8532ca40 languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.226.0" +"@aws-sdk/middleware-flexible-checksums@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.272.0" dependencies: - "@aws-crypto/crc32": 2.0.0 - "@aws-crypto/crc32c": 2.0.0 + "@aws-crypto/crc32": 3.0.0 + "@aws-crypto/crc32c": 3.0.0 "@aws-sdk/is-array-buffer": 3.201.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: c4f0531b3c917a0bfaf3b877f7f15de30f5e7e0a8366206c680efe4d0f60a0e54d7861ee14d213f713e157f164283d51724288b16fba67fef39d628324fef588 + checksum: 06d0f9edd3c78d17e0669dd913daed67f0c45d59be54bf39e68ad1184fd0dc80d544545bbe09f6dd51ca6dca8a42b585ba83e5be3aa665c6486fb5210b08f8df languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.226.0" +"@aws-sdk/middleware-host-header@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 35eb7d9f5eb2e45c58bb5887a12dc80f22e8fc630d53d356064c62ce3354c9054717697168ab3818f14c7baae3246480aa7251e6fcaf5c7104498df5dca9bbd6 + checksum: 3e191fe72de35b517a20fe47f647527579458091d4d66d55ef7e6b08107a4f4d7abe77afdcdd061776db906309d414525ff63e472c2c05d6e2447f41582f64fb languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.226.0" +"@aws-sdk/middleware-location-constraint@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 31db89d8935771aa73552fd99bc3a8fb3cca25c4e5bf978e96d05fd3aa8ab8403774ddeee08f83a4c9e5b4630082001e5d13aec1924353257fa87184dffba3e1 + checksum: 42cb878c4a2b4e1c0884277a9cacc4611111e1d6de649c4fb41f770e60ddceef27de433667c4e4d29882f6c9ad746859e53e0724fbfc5aff23720496a4007a0b languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-logger@npm:3.226.0" +"@aws-sdk/middleware-logger@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-logger@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 7b7c2eb336f376f6e8c50ebd0fd9b6e9a749e21d6f5c433683a4e98edb311bb76fafc0bb0abfd75f61001466879be7d68aefb3958a332e540160a93ee9371b44 + checksum: 128163d39522017ffb0099334f283ba82806b0c5b82965b47fff8b3b8648ae2b542b1733c69934bbca65fded9d26b8b93fa587547eeb8db2a3f5249841848f96 languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.226.0" +"@aws-sdk/middleware-recursion-detection@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 194161d76a815df623c6699f7a7016268b4a01276baa3527682504001a4e2e948e2dd389d2065f62fe57509f20b9680ea907bfa52b861122335d276c9ab4ed61 + checksum: cdc150a4f3cbd5d7c6926befcf7eb49daea1d0ca054dbe7b82e013eb74f3850056b78603f61989c2bb3cdc748798674b6811c3011c6a8c1773e13dfa11ea3ae7 languageName: node linkType: hard -"@aws-sdk/middleware-retry@npm:3.235.0": - version: 3.235.0 - resolution: "@aws-sdk/middleware-retry@npm:3.235.0" +"@aws-sdk/middleware-retry@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-retry@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/service-error-classification": 3.229.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/util-middleware": 3.226.0 - "@aws-sdk/util-retry": 3.229.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/service-error-classification": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/util-middleware": 3.272.0 + "@aws-sdk/util-retry": 3.272.0 tslib: ^2.3.1 uuid: ^8.3.2 - checksum: 397226fd10d8b0587a35ccab92160223f10740beb01ccac75dda0e30336b83c091c9aa51b9132a56ff6cccd6353b9e3c6ecf21ae18f20f8127e7e9e209e59a2a + checksum: 208e8f1b96f5ee47d28d76d0f7738f0ae7e355cb9aa24dcb88a8c4d9aa35bf833bf333c1ad4a6d11d18ec27b1713cac0ee04e0fdac17462059416a214f366944 languageName: node linkType: hard -"@aws-sdk/middleware-sdk-s3@npm:3.231.0": - version: 3.231.0 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.231.0" +"@aws-sdk/middleware-sdk-s3@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-arn-parser": 3.208.0 tslib: ^2.3.1 - checksum: d60f189d7fb4bcbbd002999f537c49f22c1e2a3e5969702a8840f17d0314f6e9eb586681995bb2b6634ae5162bebf44a3ccfecf23fdfc3c9fa6797f9adc83423 + checksum: 464f38d1593866724ce4e793e065c4e23b875ec889384a99c64d7aa34baf765ebdc18b218ed5e83f4f6c257d7b56c560aab8c0b617960a200484970ec8d55526 languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sqs@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.226.0" +"@aws-sdk/middleware-sdk-sqs@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-hex-encoding": 3.201.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: 3c3d4cfaec156ad559a2409a095e7238e9fb6c1c6cb8b5304a62b61ea6b4241df907ea4ff0b71d0f0865b8338731113946e5df8cff361f73d2725730b2aa6db0 + checksum: 2338c5865fb797f5e9f0963cf0045e991a9aa816c4fefb86943e129be68ff3b94a662741692999c2a212e4491db299241453d9b432c3eee63781850f22772a29 languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sts@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-sdk-sts@npm:3.226.0" +"@aws-sdk/middleware-sdk-sts@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-sdk-sts@npm:3.272.0" dependencies: - "@aws-sdk/middleware-signing": 3.226.0 - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/signature-v4": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/middleware-signing": 3.272.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/signature-v4": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 0b6e11889c9f1b5264eb38288dea5bd2987fa28338651939d005eaa0524f9cd41c570c545b34b08d25d858397b3f14a2b6a21238a8c88fc08719ed50b835cb15 + checksum: d808f945ae7354ef7102737418794565f26af3ca3241c3873235284400a13faf118242e92eba1831c25fc249d9c6a15f3c16e4ae3101cd6d5ab175a669b70fcb languageName: node linkType: hard -"@aws-sdk/middleware-serde@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-serde@npm:3.226.0" +"@aws-sdk/middleware-serde@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-serde@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: e8315079cc3ed4527d1434eb28becfaf951338d2ced21116753b52d3330eabbfd13f24382b3378bcc2c09ee0653a65e0d8353e9e32f512b69929908272b83fa2 + checksum: d33c07ff8c7fb9682115220ae035de5414124fd81319d7490ed3f2c235b3c0b536634a5939b311dbd735050deca4702503ee26d9e17b97ba8a1ff9e46093573e languageName: node linkType: hard -"@aws-sdk/middleware-signing@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-signing@npm:3.226.0" +"@aws-sdk/middleware-signing@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-signing@npm:3.272.0" dependencies: - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/signature-v4": 3.226.0 - "@aws-sdk/types": 3.226.0 - "@aws-sdk/util-middleware": 3.226.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/signature-v4": 3.272.0 + "@aws-sdk/types": 3.272.0 + "@aws-sdk/util-middleware": 3.272.0 tslib: ^2.3.1 - checksum: 4f66483d359e21d1f7e56a1900f7271a561801e1ee4380deaa006ce750ce73245a5fe554d4c1ab60cf9ca127c41f35c53c0817c375df6f8f068c9ed7f9572444 + checksum: 0b711de21b0f2b28178b211ae2de962676038ed49ff9247810c2213018b77ca399285278a0b448a0242f92796e1360b83c09aa268fe64a7f153428b9ed919edc languageName: node linkType: hard -"@aws-sdk/middleware-ssec@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-ssec@npm:3.226.0" +"@aws-sdk/middleware-ssec@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: b85bf0e8be723a28a03dd7375284a5afc00edb04bf333a44286042768391243670b61b78361883d7c7890b118e0dc995219d7870227f845ac868a756e8a76a70 + checksum: 81836fd3235f455d79d7cdc0bc193b321024dab2f6443f16e013c5eb4b3e06c3ca283152cb560f7f2bd2e2accf81f773f95278e8c9094806c1ddb54b5af0db8f languageName: node linkType: hard -"@aws-sdk/middleware-stack@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-stack@npm:3.226.0" +"@aws-sdk/middleware-stack@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-stack@npm:3.272.0" dependencies: tslib: ^2.3.1 - checksum: fd284bc28ee6ee576e3fe9dd3c2c9d64b8c95fa1bb213b5e41e23a57bbc2693652ee51028414eb278918235e591ef729db572b160bff963c6f74e7d6c15d33d0 + checksum: 7006c4168c2e5c7b15edd4e87001a6e4edbd2f549908ac0e68bb61f12c232048ac1d351b0714e7a79115aeaf251838877b2ef9ac829a6b1ab04913015a3c87a3 languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.226.0" +"@aws-sdk/middleware-user-agent@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 8d3724aecd7b7f9f1a5b081263cb85864c969144105819f371a9675ee4f680c11e3b5ecbe181f57d2bfdde9c98b90267bd4afe0fc0822863041e0df52671f043 + checksum: a392a8c68509f118f008002e4ef6ba7fea9f93ade97963449e6eec951c0319a7cc8fd40cf77bda1540ff25ebbbe75785a6fc5ed4be63676144e5242892940732 languageName: node linkType: hard -"@aws-sdk/node-config-provider@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/node-config-provider@npm:3.226.0" +"@aws-sdk/node-config-provider@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/node-config-provider@npm:3.272.0" dependencies: - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/shared-ini-file-loader": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/shared-ini-file-loader": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 0e7c4c7ea20c5361c71f1ab0b99c19964f76c187659714cd6e87e1cc264d0ab29965d33618d9d47d373e3b108558cd6628fb36a00cdac614849e5044c6776213 + checksum: 57f1f107ae4e213ffedf648d46fe0e026af83b13eeb8c13bd8f812cbbb226a0b2c2793afec0e29165ee64cc948182499ad44fe62f63ac58b06d0dd2cab7f1461 languageName: node linkType: hard -"@aws-sdk/node-http-handler@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/node-http-handler@npm:3.226.0" +"@aws-sdk/node-http-handler@npm:3.272.0, @aws-sdk/node-http-handler@npm:^3.208.0": + version: 3.272.0 + resolution: "@aws-sdk/node-http-handler@npm:3.272.0" dependencies: - "@aws-sdk/abort-controller": 3.226.0 - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/querystring-builder": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/abort-controller": 3.272.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/querystring-builder": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: e4080ec9f859be4b191416084fef8d2687f24d973169b8fef592a867c23c14fad74f4bea20f451b98fedfee6a265f2c89b0bee4fe7aabdb1fd05a7475bf12b91 + checksum: cbcdab82a7611d1d776197e921563ac6248173aa6e5a6f6e6486a735c71763ae69d1d6ae6de8acebcb8020cdfa089a776d9180929397291623db8f11a1f7b8a9 languageName: node linkType: hard -"@aws-sdk/property-provider@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/property-provider@npm:3.226.0" +"@aws-sdk/property-provider@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/property-provider@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: d63c53313bb2797ab2ecfa2cff827b63417c0cc31abe8c666cd018a1d70da877b61ef1fb21e504d6408e72b20544756089938b44869b003ba3c313b39bca6d45 + checksum: 7f01794e93276e13339034dac816b2b5da2370fbc1ff304d451e2d0344957e94ba53f5c54de44eca4cb53e2847c7be820db25d6164ca9e489a4a39787fb6c8b3 languageName: node linkType: hard @@ -1254,13 +1244,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/protocol-http@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/protocol-http@npm:3.226.0" +"@aws-sdk/protocol-http@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/protocol-http@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 92cf35e1026a812c6c7ce8309fd685162ab7796237fc063e71dd9b292abf25902d6c0e02e93219101e1f506ef68a98f02eed6a61b18b8acd450ecb4bf8c1b467 + checksum: 59d7c97b1a46bc3c072f56caec365472c9d94638923799ad2418cee52bb0eab945ab080cd63f5351ff45ad943dde9d88b294e096af36524e184c0764b2ea6cc5 languageName: node linkType: hard @@ -1275,51 +1265,51 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/querystring-builder@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/querystring-builder@npm:3.226.0" +"@aws-sdk/querystring-builder@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/querystring-builder@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-uri-escape": 3.201.0 tslib: ^2.3.1 - checksum: 0014b8876d402787c56f606f66e79eb48ff100b01d7a657bb0766a7723ca184da0dd2aed405837e7e6c5781fd868d7a903eef25884b2d1881d30e2454f5c2bfc + checksum: 8164bb5547c556a2b3933b0a93d55d9f205ebe096f2dbcea6d3be4aa23bb0b4e7996ba24363b9ac3f80b4026903b267b759efdf9f72379f1eea9250c182238b9 languageName: node linkType: hard -"@aws-sdk/querystring-parser@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/querystring-parser@npm:3.226.0" +"@aws-sdk/querystring-parser@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/querystring-parser@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 13b23b4dff859d778a05f6874e318d95d6fbd79c62a06bb6f6dff8496ac2eaf86f60b17460c3595af854c2e316482c9fcc098655d475926039f7dff4a6f1ecf1 + checksum: fe5a29a265f48ad02b3161e888a66e4ac08fe07d52a67e811ac1da1734bfa9ac87ead637b595efcbc163f64f2280c8b56c867a8399f4c816b3683b6308837f93 languageName: node linkType: hard -"@aws-sdk/service-error-classification@npm:3.229.0": - version: 3.229.0 - resolution: "@aws-sdk/service-error-classification@npm:3.229.0" - checksum: 7877963d62f4d43ce5116f0ddfd1b27cffab2b0e74a4ceb4006b1f85b17dd50eb9ae2f227cabf935261e42b7615d0a9de190dd684380c30e03dee6e9f7e12587 +"@aws-sdk/service-error-classification@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/service-error-classification@npm:3.272.0" + checksum: 9507f74c909a968aa9bf835a05a9cfc93a0892e7c685de37da485e74a61b4fff271621d7c841aa0c9aedbcc57b19f171e6bc74925a82007be6c6750b3c7d0198 languageName: node linkType: hard -"@aws-sdk/shared-ini-file-loader@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/shared-ini-file-loader@npm:3.226.0" +"@aws-sdk/shared-ini-file-loader@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/shared-ini-file-loader@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 7b43e4d70f33b1098c9582a20dd5cf11c0e2efe5e7f4f854ee1fdfe2f804a7171deeb76f167c46ef7365bb584e29c816d58b6d8f6c9ac9debb124cccf59ebf59 + checksum: 1245658ad6efbeda0181aca64b00259749abc76d6200ac8fb481fe627eb88932a5cf2e9c366583ccba9fa540706a08b611552fff5233d66835c0e525c51f97c4 languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.226.0" +"@aws-sdk/signature-v4-multi-region@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.272.0" dependencies: - "@aws-sdk/protocol-http": 3.226.0 - "@aws-sdk/signature-v4": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/protocol-http": 3.272.0 + "@aws-sdk/signature-v4": 3.272.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-arn-parser": 3.208.0 tslib: ^2.3.1 peerDependencies: @@ -1327,45 +1317,46 @@ __metadata: peerDependenciesMeta: "@aws-sdk/signature-v4-crt": optional: true - checksum: 173645989956907bbd83c4098e2e52f977272073713ac894ec7bf74f84ddb071c2142b2f5edfb7f7f939a60b8d8b5140bd114e6b028bb5216c341da7eb910ea2 + checksum: 231eac2d4203a9ed3829d55a93d503d629bbd43301516d4e06c461dd16eba3a72373c004a59789de43487804266ca4ced259adcb1d26b6916f7b3545401ff544 languageName: node linkType: hard -"@aws-sdk/signature-v4@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/signature-v4@npm:3.226.0" +"@aws-sdk/signature-v4@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/signature-v4@npm:3.272.0" dependencies: "@aws-sdk/is-array-buffer": 3.201.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-hex-encoding": 3.201.0 - "@aws-sdk/util-middleware": 3.226.0 + "@aws-sdk/util-middleware": 3.272.0 "@aws-sdk/util-uri-escape": 3.201.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: 8bc6ea1ad467d59f1ff6c5b814277e16d0d5b2d1455f21969b4575f572fdc013284e356e0f24436c89c8b99a51d0e384b01daa2bace1c3d69e28f35a23ef9802 + checksum: bdf997ed7779e173797bd31a9ae39bc9860e12cb7d0a734b4a47c76bde435c5c1d5f56dbb3c5f1a2b26a828367fd88f34a4527b8a12d4c1a60b3b0049aacf706 languageName: node linkType: hard -"@aws-sdk/smithy-client@npm:3.234.0": - version: 3.234.0 - resolution: "@aws-sdk/smithy-client@npm:3.234.0" +"@aws-sdk/smithy-client@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/smithy-client@npm:3.272.0" dependencies: - "@aws-sdk/middleware-stack": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/middleware-stack": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 7b8299f81fde410b8f597662d4174a29740cb2791a2377e5009a73c0e7531fb440712ab25c837a0351dbceac4763cc8fadc6d4362727cf3ae749b25515da4ea1 + checksum: 8ba3c4733a32c7c28ec06735b0fc3dbc33a1657bfc54dc28f6770b021fda74680c693812d8c6db2291197a275f32fd80be909dddda4eb726328bf34e8d18bccc languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.245.0": - version: 3.245.0 - resolution: "@aws-sdk/token-providers@npm:3.245.0" +"@aws-sdk/token-providers@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/token-providers@npm:3.272.0" dependencies: - "@aws-sdk/client-sso-oidc": 3.245.0 - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/shared-ini-file-loader": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/client-sso-oidc": 3.272.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/shared-ini-file-loader": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 900a73b999c50eb35b3eda594210448329f5068b57a2d8d57ee8e4f3d960116ece36952ca1fbe43b5e8e9119927de916033f73ff969bae3a8394d4ef98f1d0c7 + checksum: 8d49846f4ad124707879a9b8adaea089c4d02dfd1c15dcceb083fecd538ecce1db65f1104e6178b13941acb2a4ae275c5cf9eb586d5d25757eb9fb454bdbb70c languageName: node linkType: hard @@ -1376,23 +1367,23 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/types@npm:3.226.0, @aws-sdk/types@npm:^3.1.0, @aws-sdk/types@npm:^3.110.0, @aws-sdk/types@npm:^3.208.0": - version: 3.226.0 - resolution: "@aws-sdk/types@npm:3.226.0" +"@aws-sdk/types@npm:3.272.0, @aws-sdk/types@npm:^3.208.0, @aws-sdk/types@npm:^3.222.0": + version: 3.272.0 + resolution: "@aws-sdk/types@npm:3.272.0" dependencies: tslib: ^2.3.1 - checksum: 0041a8c0924ec7ba4ff787ae329a80b3edb9ace43e38fe3656a1862ae5324427fb09836275c0962cbc80e3c34a6ee512a108a5c4e7997f29664e3e8930cffd80 + checksum: c4e4f09dd2672eab4fc15b08006c730c1d07d4b54ae35e03167d99f6110751e36b5332165e03c71c28724037f623f8da87a45a018eb631a1ce86d406f9d08da6 languageName: node linkType: hard -"@aws-sdk/url-parser@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/url-parser@npm:3.226.0" +"@aws-sdk/url-parser@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/url-parser@npm:3.272.0" dependencies: - "@aws-sdk/querystring-parser": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/querystring-parser": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: cb8d4b13c0de7336728a7a60a246108c0ecd46e5e493cb1006f5ab58c5dbae0ac36a791536786b1facc90f58a8d6decca9f4fda2cba19667ba5a9f5f4fcadb63 + checksum: 4a66e733b336b0e28eceef89271f255dcd7079cfa8ba1c854e6c147ced211242e00016e3f22752e5ea3e49b35457568c5ae8df574f729d6e60cd1f5054f5a57d languageName: node linkType: hard @@ -1452,39 +1443,39 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-defaults-mode-browser@npm:3.234.0": - version: 3.234.0 - resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.234.0" +"@aws-sdk/util-defaults-mode-browser@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.272.0" dependencies: - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/types": 3.272.0 bowser: ^2.11.0 tslib: ^2.3.1 - checksum: 79a381a632f867477dc7f948243c8ba82932d3f10325c76a5f2cf96412560648d149ba43ee31794bc45d81158a8c361d890b291880fc0c05f9bafc99cded5ef4 + checksum: 56594cc954c6431ced3646668fd92d8229eb1c20a37d6c66b0ebf3ce88a7f9803c63d432c78412768ebf0d141fdacef01644ea0f3507e6fb295a22c80f728c1a languageName: node linkType: hard -"@aws-sdk/util-defaults-mode-node@npm:3.234.0": - version: 3.234.0 - resolution: "@aws-sdk/util-defaults-mode-node@npm:3.234.0" +"@aws-sdk/util-defaults-mode-node@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-defaults-mode-node@npm:3.272.0" dependencies: - "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/credential-provider-imds": 3.226.0 - "@aws-sdk/node-config-provider": 3.226.0 - "@aws-sdk/property-provider": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/config-resolver": 3.272.0 + "@aws-sdk/credential-provider-imds": 3.272.0 + "@aws-sdk/node-config-provider": 3.272.0 + "@aws-sdk/property-provider": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: b70becd9e561c1622c75a452ed8c7a8b14b84eba35988159f29476db8c2b47ec72f0b7c19d1a252c725756af1fff1df4201e5e92b6136d1df9170e806dca5219 + checksum: ee0e44d78d70b6eab73d6e8455972218cb1a15950f47d81d72d094420b2719add34bf46218016f85242a2a155216d2a0542ed5f4e68e171efc09691125fcb6fd languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:3.245.0": - version: 3.245.0 - resolution: "@aws-sdk/util-endpoints@npm:3.245.0" +"@aws-sdk/util-endpoints@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-endpoints@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: 22783fb178e8901385b20b0d19b944ad41297cc19f0561ac1722e86757ca6910fd3766626a0493372f718a0726bef920a5b39cc41fdceb1012f14e833541041b + checksum: d701e0245930df2000974f9d7c8398fde65924fe5aaa72f2c314fa40a8842e1868a6e285aa78479f741e40b8be54060a4607c9b5b3ac0386839a45df8df238f2 languageName: node linkType: hard @@ -1506,48 +1497,48 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-middleware@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/util-middleware@npm:3.226.0" +"@aws-sdk/util-middleware@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-middleware@npm:3.272.0" dependencies: tslib: ^2.3.1 - checksum: 51a4ba9a784943b723a2c57bbb889a4bc743f0f1219000ef73512e259069d63c3baa805aa375277c9d904da23301d0389bc0bd525b0b55274d7658fb62173e55 + checksum: e9a0b55317c474d3eecd65765443eff68f5130ac6f982a41450e0cb949169ff2cf13273282337f2067dfdb5cc7d25137b3bd2fa5f5228cfe541003cf387a6101 languageName: node linkType: hard -"@aws-sdk/util-retry@npm:3.229.0": - version: 3.229.0 - resolution: "@aws-sdk/util-retry@npm:3.229.0" +"@aws-sdk/util-retry@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-retry@npm:3.272.0" dependencies: - "@aws-sdk/service-error-classification": 3.229.0 + "@aws-sdk/service-error-classification": 3.272.0 tslib: ^2.3.1 - checksum: e4ab435000abfa1dc21153682ce684302bd6e69d48b7d2c876ff8dfa66793a43223961a3755d05abb1f94f82765eac2b9d48e55c688a957a7f6b5d032bbc46ee + checksum: 7449d9b5daef1244a1601c9892bdd57abeb71ee87357e9dac71d53a77b80648b8cd0db1e99749a76e241ab01fce71a753360d0c8e130f6ae6e71a94b462242f0 languageName: node linkType: hard -"@aws-sdk/util-stream-browser@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/util-stream-browser@npm:3.226.0" +"@aws-sdk/util-stream-browser@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-stream-browser@npm:3.272.0" dependencies: - "@aws-sdk/fetch-http-handler": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/fetch-http-handler": 3.272.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-base64": 3.208.0 "@aws-sdk/util-hex-encoding": 3.201.0 - "@aws-sdk/util-utf8-browser": 3.188.0 + "@aws-sdk/util-utf8": 3.254.0 tslib: ^2.3.1 - checksum: 7e232bf20733618b25f1dfee93d53a397cf22bdc7191e920b649837773fc6e551c1671f71581d6294d9e77c4b00dabb7baa182e85cedad470642e743a5b05a5c + checksum: 18e3dfa4efcf12e5e4eda55da14c9a98154f063b1bbf0a926fc406ffdf791363a1881e93194fa5a160e7bd26dd1b1933d6a57b19134e6f0a4feda8c6f5cfcb13 languageName: node linkType: hard -"@aws-sdk/util-stream-node@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/util-stream-node@npm:3.226.0" +"@aws-sdk/util-stream-node@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-stream-node@npm:3.272.0" dependencies: - "@aws-sdk/node-http-handler": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/node-http-handler": 3.272.0 + "@aws-sdk/types": 3.272.0 "@aws-sdk/util-buffer-from": 3.208.0 tslib: ^2.3.1 - checksum: 91ed91380582bb1989d7420d4939af240fcaf0805845a308ebb9917a1b5da3cae23ebcc9963fcd20c362467130951fc13666e181eb8d40da42f213eaebcada00 + checksum: 7fa500251680e3eb5fa63d47c6d4846ca66f8c781ee0d990ed79d16a12b0bb074eb7d5c9e26390afd7a18159a758fbc333ce1b788c2a8dea7438cbe2f4c6c7c3 languageName: node linkType: hard @@ -1565,30 +1556,30 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.226.0" +"@aws-sdk/util-user-agent-browser@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.272.0" dependencies: - "@aws-sdk/types": 3.226.0 + "@aws-sdk/types": 3.272.0 bowser: ^2.11.0 tslib: ^2.3.1 - checksum: b89b63e03636b98ccb811e713776c8ee5ce92a280edfd429e418dc69bb55b770b47d6118419be76207afe137f293fdb2e87605922714189df8ddbd4573185717 + checksum: e5ac1b88f9af45c2e0dbec40d2aa9c616634df252ac437f2dfd3aeb316fe561a5c09d481c075945843b3a7f5edafbc10ef26741c6ccacc4d435d8be7eeab179c languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.226.0" +"@aws-sdk/util-user-agent-node@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.272.0" dependencies: - "@aws-sdk/node-config-provider": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/node-config-provider": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 peerDependencies: aws-crt: ">=1.0.0" peerDependenciesMeta: aws-crt: optional: true - checksum: a3b620fa13e5e61bc874d777d451ec522f659f933b7d7f7642c10fbb12ce13dd068c2b5370fd6132cca5ddcd22243fb04b4e926a4dd8aedeb08270a9bb49ee74 + checksum: 25d5f39349190e6323f893de4f0e7a931751bf1521808210515ef8d951c1a6a3e5f1dc1773cbd5ca9f999c5d00f3a421ad570b5f79b20dc2692012a9f833ff24 languageName: node linkType: hard @@ -1601,24 +1592,24 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-utf8-node@npm:3.208.0": - version: 3.208.0 - resolution: "@aws-sdk/util-utf8-node@npm:3.208.0" +"@aws-sdk/util-utf8@npm:3.254.0": + version: 3.254.0 + resolution: "@aws-sdk/util-utf8@npm:3.254.0" dependencies: "@aws-sdk/util-buffer-from": 3.208.0 tslib: ^2.3.1 - checksum: 0f4c1d0d3f6eb40d92bc7163c6dcf97f90b3a8be05df46ba4f9bb7723a3e6acabf2cb0f2486411b03d22e8fcccae9e757ed02afad384ff5f44a3a659a78c5752 + checksum: e5dfe7565f2de32245a544d1d715d803025bc5522538c0206fa61377f747804d95fc2e5e25976144bb63a6857e360b4286d101e730ab5d39866c60383a47e7d5 languageName: node linkType: hard -"@aws-sdk/util-waiter@npm:3.226.0": - version: 3.226.0 - resolution: "@aws-sdk/util-waiter@npm:3.226.0" +"@aws-sdk/util-waiter@npm:3.272.0": + version: 3.272.0 + resolution: "@aws-sdk/util-waiter@npm:3.272.0" dependencies: - "@aws-sdk/abort-controller": 3.226.0 - "@aws-sdk/types": 3.226.0 + "@aws-sdk/abort-controller": 3.272.0 + "@aws-sdk/types": 3.272.0 tslib: ^2.3.1 - checksum: d182033937bd5ab024598a48ad28030267c1b9aa54175f24d0e57ea87975a0a07b5da7d0e5f43efb663e908a5df591dfa307bf9707a02d84eb49b089e9c5a5db + checksum: e931a9359dc582db9e5dcd62539572aecff5320ce424e5462c2f8d4a2d83acde07f87fe9ffc83850aaf70eaf17b06b6ffd4554350bab0f0f3a4427d1b7e776c6 languageName: node linkType: hard @@ -2233,12 +2224,12 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.8, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.19.1, @babel/parser@npm:^7.9.4": - version: 7.19.1 - resolution: "@babel/parser@npm:7.19.1" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.8, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.19.1, @babel/parser@npm:^7.20.15": + version: 7.20.15 + resolution: "@babel/parser@npm:7.20.15" bin: parser: ./bin/babel-parser.js - checksum: b1e0acb346b2a533c857e1e97ac0886cdcbd76aafef67835a2b23f760c10568eb53ad8a27dd5f862d8ba4e583742e6067f107281ccbd68959d61bc61e4ddaa51 + checksum: 1d0f47ca67ff2652f1c0ff1570bed8deccbc4b53509e7cd73476af9cc7ed23480c99f1179bd6d0be01612368b92b39e206d330ad6054009d699934848a89298b languageName: node linkType: hard @@ -3307,12 +3298,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.20.7 - resolution: "@babel/runtime@npm:7.20.7" +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.20.13 + resolution: "@babel/runtime@npm:7.20.13" dependencies: regenerator-runtime: ^0.13.11 - checksum: 4629ce5c46f06cca9cfb9b7fc00d48003335a809888e2b91ec2069a2dcfbfef738480cff32ba81e0b7c290f8918e5c22ddcf2b710001464ee84ba62c7e32a3a3 + checksum: 09b7a97a05c80540db6c9e4ddf8c5d2ebb06cae5caf3a87e33c33f27f8c4d49d9c67a2d72f1570e796045288fad569f98a26ceba0c4f5fad2af84b6ad855c4fb languageName: node linkType: hard @@ -3387,6 +3378,7 @@ __metadata: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/config": "workspace:^" @@ -3431,6 +3423,7 @@ __metadata: resolution: "@backstage/backend-common@workspace:packages/backend-common" dependencies: "@backstage/backend-app-api": "workspace:^" + "@backstage/backend-dev-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -3443,7 +3436,7 @@ __metadata: "@google-cloud/storage": ^6.0.0 "@keyv/memcache": ^1.3.5 "@keyv/redis": ^2.5.3 - "@kubernetes/client-node": 0.18.0 + "@kubernetes/client-node": 0.18.1 "@manypkg/get-packages": ^1.1.3 "@octokit/rest": ^19.0.3 "@types/archiver": ^5.1.0 @@ -3492,7 +3485,7 @@ __metadata: minimist: ^1.2.5 mock-fs: ^5.1.0 morgan: ^1.10.0 - msw: ^0.49.0 + msw: ^1.0.0 mysql2: ^2.2.5 node-fetch: ^2.6.7 node-forge: ^1.3.1 @@ -3521,12 +3514,21 @@ __metadata: resolution: "@backstage/backend-defaults@workspace:packages/backend-defaults" dependencies: "@backstage/backend-app-api": "workspace:^" + "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" languageName: unknown linkType: soft +"@backstage/backend-dev-utils@workspace:^, @backstage/backend-dev-utils@workspace:packages/backend-dev-utils": + version: 0.0.0-use.local + resolution: "@backstage/backend-dev-utils@workspace:packages/backend-dev-utils" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/backend-plugin-api@workspace:^, @backstage/backend-plugin-api@workspace:packages/backend-plugin-api": version: 0.0.0-use.local resolution: "@backstage/backend-plugin-api@workspace:packages/backend-plugin-api" @@ -3581,7 +3583,7 @@ __metadata: better-sqlite3: ^8.0.0 express: ^4.17.1 knex: ^2.0.0 - msw: ^0.49.0 + msw: ^1.0.0 mysql2: ^2.2.5 pg: ^8.3.0 supertest: ^6.1.3 @@ -3590,6 +3592,17 @@ __metadata: languageName: unknown linkType: soft +"@backstage/catalog-client@npm:^1.3.1": + version: 1.3.1 + resolution: "@backstage/catalog-client@npm:1.3.1" + dependencies: + "@backstage/catalog-model": ^1.2.0 + "@backstage/errors": ^1.1.4 + cross-fetch: ^3.1.5 + checksum: e2e0050d93f509fbaed40d89228ce9af7c9f32372c721d5faea29ef312e488cbfc1426959f39d61790d41406fbd6bc895bed2c3427bbdf6cab6a412f449c2efe + languageName: node + linkType: hard + "@backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": version: 0.0.0-use.local resolution: "@backstage/catalog-client@workspace:packages/catalog-client" @@ -3598,11 +3611,26 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/errors": "workspace:^" cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 languageName: unknown linkType: soft -"@backstage/catalog-model@^1.1.3, @backstage/catalog-model@^1.1.4, @backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": +"@backstage/catalog-model@npm:^1.1.3, @backstage/catalog-model@npm:^1.1.5, @backstage/catalog-model@npm:^1.2.0": + version: 1.2.0 + resolution: "@backstage/catalog-model@npm:1.2.0" + dependencies: + "@backstage/config": ^1.0.6 + "@backstage/errors": ^1.1.4 + "@backstage/types": ^1.0.2 + ajv: ^8.10.0 + json-schema: ^0.4.0 + lodash: ^4.17.21 + uuid: ^8.0.0 + checksum: d4e07e91e501b4ecbc9d0b754518277dd2ed93f22f801740cc539dbcbd905ba8111b1651d20fb725e44d7cd99a96faec6fd56cd51fdf8e1d7f39a6e177f31efa + languageName: node + linkType: hard + +"@backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": version: 0.0.0-use.local resolution: "@backstage/catalog-model@workspace:packages/catalog-model" dependencies: @@ -3642,10 +3670,12 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/eslint-plugin": "workspace:^" "@backstage/release-manifests": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" + "@esbuild-kit/cjs-loader": ^2.4.1 "@manypkg/get-packages": ^1.1.3 "@octokit/request": ^6.0.0 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.7 @@ -3664,6 +3694,7 @@ __metadata: "@swc/core": ^1.3.9 "@swc/helpers": ^0.4.7 "@swc/jest": ^0.2.22 + "@types/cross-spawn": ^6.0.2 "@types/diff": ^5.0.0 "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.1 @@ -3691,10 +3722,11 @@ __metadata: chalk: ^4.0.0 chokidar: ^3.3.1 commander: ^9.1.0 + cross-spawn: ^7.0.3 css-loader: ^6.5.1 - del: ^6.0.0 + del: ^7.0.0 diff: ^5.0.0 - esbuild: ^0.16.0 + esbuild: ^0.17.0 esbuild-loader: ^2.18.0 eslint: ^8.6.0 eslint-config-prettier: ^8.3.0 @@ -3703,7 +3735,6 @@ __metadata: eslint-plugin-import: ^2.25.4 eslint-plugin-jest: ^27.0.0 eslint-plugin-jsx-a11y: ^6.5.1 - eslint-plugin-monorepo: ^0.3.2 eslint-plugin-react: ^7.28.0 eslint-plugin-react-hooks: ^4.3.0 eslint-webpack-plugin: ^3.1.1 @@ -3724,7 +3755,7 @@ __metadata: mini-css-extract-plugin: ^2.4.2 minimatch: ^5.1.1 mock-fs: ^5.1.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 node-libs-browser: ^2.2.1 nodemon: ^2.0.2 @@ -3806,7 +3837,7 @@ __metadata: json-schema-merge-allof: ^0.8.1 json-schema-traverse: ^1.0.0 mock-fs: ^5.1.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 typescript-json-schema: ^0.55.0 yaml: ^2.0.0 @@ -3814,7 +3845,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/config@^1.0.4, @backstage/config@workspace:^, @backstage/config@workspace:packages/config": +"@backstage/config@^1.0.4, @backstage/config@^1.0.6, @backstage/config@workspace:^, @backstage/config@workspace:packages/config": version: 0.0.0-use.local resolution: "@backstage/config@workspace:packages/config" dependencies: @@ -3844,7 +3875,7 @@ __metadata: "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 prop-types: ^15.7.2 react-router-beta: "npm:react-router@6.0.0-beta.0" react-router-dom-beta: "npm:react-router-dom@6.0.0-beta.0" @@ -3860,7 +3891,57 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-components@^0.12.0, @backstage/core-components@^0.12.2, @backstage/core-components@workspace:^, @backstage/core-components@workspace:packages/core-components": +"@backstage/core-components@npm:^0.12.0, @backstage/core-components@npm:^0.12.3, @backstage/core-components@npm:^0.12.4": + version: 0.12.4 + resolution: "@backstage/core-components@npm:0.12.4" + dependencies: + "@backstage/config": ^1.0.6 + "@backstage/core-plugin-api": ^1.4.0 + "@backstage/errors": ^1.1.4 + "@backstage/theme": ^0.2.17 + "@backstage/version-bridge": ^1.0.3 + "@material-table/core": ^3.1.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + "@react-hookz/web": ^20.0.0 + "@types/react-sparklines": ^1.7.0 + "@types/react-text-truncate": ^0.14.0 + ansi-regex: ^6.0.1 + classnames: ^2.2.6 + d3-selection: ^3.0.0 + d3-shape: ^3.0.0 + d3-zoom: ^3.0.0 + dagre: ^0.8.5 + history: ^5.0.0 + immer: ^9.0.1 + lodash: ^4.17.21 + pluralize: ^8.0.0 + prop-types: ^15.7.2 + qs: ^6.9.4 + rc-progress: 3.4.1 + react-helmet: 6.1.0 + react-hook-form: ^7.12.2 + react-markdown: ^8.0.0 + react-sparklines: ^1.7.0 + react-syntax-highlighter: ^15.4.5 + react-text-truncate: ^0.19.0 + react-use: ^17.3.2 + react-virtualized-auto-sizer: ^1.0.6 + react-window: ^1.8.6 + remark-gfm: ^3.0.1 + zen-observable: ^0.10.0 + zod: ~3.18.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-dom: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 476160ad54a399e7393bb70fc34fc01b69519431606797fdecc974d5e826315527d8ab71af5dd4d1c8d88bfe797aa37e781fd5fd31d8293c19982704e4f7cfdc + languageName: node + linkType: hard + +"@backstage/core-components@workspace:^, @backstage/core-components@workspace:packages/core-components": version: 0.0.0-use.local resolution: "@backstage/core-components@workspace:packages/core-components" dependencies: @@ -3906,7 +3987,7 @@ __metadata: history: ^5.0.0 immer: ^9.0.1 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 pluralize: ^8.0.0 prop-types: ^15.7.2 qs: ^6.9.4 @@ -3931,7 +4012,25 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-plugin-api@^1.1.0, @backstage/core-plugin-api@^1.2.0, @backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": +"@backstage/core-plugin-api@npm:^1.1.0, @backstage/core-plugin-api@npm:^1.3.0, @backstage/core-plugin-api@npm:^1.4.0": + version: 1.4.0 + resolution: "@backstage/core-plugin-api@npm:1.4.0" + dependencies: + "@backstage/config": ^1.0.6 + "@backstage/types": ^1.0.2 + "@backstage/version-bridge": ^1.0.3 + history: ^5.0.0 + prop-types: ^15.7.2 + zen-observable: ^0.10.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 046d0fcb52b3110d6a926eeaf766486c753c2eeca70f7897f4ded45e0cf7da8591ec7df337433427bc50de7193e7a6d875356a027a8d1733fa07d87509e561a1 + languageName: node + linkType: hard + +"@backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": version: 0.0.0-use.local resolution: "@backstage/core-plugin-api@workspace:packages/core-plugin-api" dependencies: @@ -3950,7 +4049,7 @@ __metadata: "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 history: ^5.0.0 - msw: ^0.49.0 + msw: ^1.0.0 prop-types: ^15.7.2 zen-observable: ^0.10.0 peerDependencies: @@ -3960,7 +4059,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/create-app@workspace:*, @backstage/create-app@workspace:packages/create-app": +"@backstage/create-app@workspace:*, @backstage/create-app@workspace:^, @backstage/create-app@workspace:packages/create-app": version: 0.0.0-use.local resolution: "@backstage/create-app@workspace:packages/create-app" dependencies: @@ -4016,7 +4115,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/errors@workspace:^, @backstage/errors@workspace:packages/errors": +"@backstage/errors@^1.1.4, @backstage/errors@workspace:^, @backstage/errors@workspace:packages/errors": version: 0.0.0-use.local resolution: "@backstage/errors@workspace:packages/errors" dependencies: @@ -4027,6 +4126,17 @@ __metadata: languageName: unknown linkType: soft +"@backstage/eslint-plugin@workspace:^, @backstage/eslint-plugin@workspace:packages/eslint-plugin": + version: 0.0.0-use.local + resolution: "@backstage/eslint-plugin@workspace:packages/eslint-plugin" + dependencies: + "@backstage/cli": "workspace:^" + "@manypkg/get-packages": ^1.1.3 + eslint: ^8.33.0 + minimatch: ^5.1.2 + languageName: unknown + linkType: soft + "@backstage/integration-aws-node@workspace:^, @backstage/integration-aws-node@workspace:packages/integration-aws-node": version: 0.0.0-use.local resolution: "@backstage/integration-aws-node@workspace:packages/integration-aws-node" @@ -4046,7 +4156,26 @@ __metadata: languageName: unknown linkType: soft -"@backstage/integration-react@^1.1.8, @backstage/integration-react@workspace:^, @backstage/integration-react@workspace:packages/integration-react": +"@backstage/integration-react@npm:^1.1.9": + version: 1.1.10 + resolution: "@backstage/integration-react@npm:1.1.10" + dependencies: + "@backstage/config": ^1.0.6 + "@backstage/core-components": ^0.12.4 + "@backstage/core-plugin-api": ^1.4.0 + "@backstage/integration": ^1.4.2 + "@backstage/theme": ^0.2.17 + "@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 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + checksum: 248e11c74fed7fb6d5fcc9cf7f7d7baffe94a6a4091aff60afc46c797b08a63486baaffe32812c237e29e3cb295d429b1cd740ca704b43b7dc4704a910184716 + languageName: node + linkType: hard + +"@backstage/integration-react@workspace:^, @backstage/integration-react@workspace:packages/integration-react": version: 0.0.0-use.local resolution: "@backstage/integration-react@workspace:packages/integration-react" dependencies: @@ -4066,14 +4195,14 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 languageName: unknown linkType: soft -"@backstage/integration@workspace:^, @backstage/integration@workspace:packages/integration": +"@backstage/integration@^1.4.2, @backstage/integration@workspace:^, @backstage/integration@workspace:packages/integration": version: 0.0.0-use.local resolution: "@backstage/integration@workspace:packages/integration" dependencies: @@ -4089,7 +4218,7 @@ __metadata: git-url-parse: ^13.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 languageName: unknown linkType: soft @@ -4106,14 +4235,14 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-adr-common": "workspace:^" "@backstage/plugin-search-common": "workspace:^" - "@types/express": ^4.17.15 + "@types/express": ^4.17.6 "@types/marked": ^4.0.0 "@types/supertest": ^2.0.8 - express: ^4.18.2 - express-promise-router: ^4.1.1 + express: ^4.17.1 + express-promise-router: ^4.1.0 luxon: ^3.0.0 marked: ^4.0.14 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.5 supertest: ^6.1.3 winston: ^3.2.1 @@ -4158,11 +4287,12 @@ __metadata: "@types/git-url-parse": ^9.0.0 "@types/node": "*" cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-markdown: ^8.0.0 react-use: ^17.2.4 remark-gfm: ^3.0.1 peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 languageName: unknown @@ -4181,7 +4311,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 http-proxy-middleware: ^2.0.0 - msw: ^0.49.0 + msw: ^1.0.0 supertest: ^6.1.6 winston: ^3.2.1 yn: ^4.0.0 @@ -4209,7 +4339,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4238,7 +4368,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4266,7 +4396,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-ga: ^3.3.0 react-use: ^17.2.4 peerDependencies: @@ -4292,7 +4422,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.10.1 react-use: ^17.2.4 peerDependencies: @@ -4317,7 +4447,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-api-docs@workspace:plugins/api-docs" dependencies: - "@asyncapi/react-component": 1.0.0-next.44 + "@asyncapi/react-component": 1.0.0-next.47 "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" @@ -4341,7 +4471,7 @@ __metadata: graphql: ^16.0.0 graphql-ws: ^5.4.1 isomorphic-form-data: ^2.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 swagger-ui-react: ^4.11.1 peerDependencies: @@ -4363,7 +4493,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@material-ui/core": ^4.9.13 + "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": ^4.0.0-alpha.57 "@testing-library/jest-dom": ^5.10.1 @@ -4371,7 +4501,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": "*" cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 use-deep-compare-effect: ^1.8.1 peerDependencies: @@ -4402,7 +4532,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 mock-fs: ^5.1.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.1.3 winston: ^3.2.1 @@ -4422,6 +4552,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@davidzemon/passport-okta-oauth": ^0.0.5 "@google-cloud/firestore": ^6.0.0 @@ -4434,7 +4565,7 @@ __metadata: "@types/passport-auth0": ^1.0.5 "@types/passport-github2": ^1.2.4 "@types/passport-google-oauth20": ^2.0.3 - "@types/passport-microsoft": ^0.0.0 + "@types/passport-microsoft": ^1.0.0 "@types/passport-saml": ^1.1.3 "@types/passport-strategy": ^0.2.35 "@types/xml2js": ^0.4.7 @@ -4453,7 +4584,7 @@ __metadata: luxon: ^3.0.0 minimatch: ^5.0.0 morgan: ^1.10.0 - msw: ^0.49.0 + msw: ^1.0.0 node-cache: ^5.1.2 node-fetch: ^2.6.7 openid-client: ^5.2.1 @@ -4487,7 +4618,7 @@ __metadata: express: ^4.17.1 jose: ^4.6.0 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -4508,7 +4639,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 mime-types: ^2.1.27 - msw: ^0.49.0 + msw: ^1.0.0 p-limit: ^3.1.0 supertest: ^6.1.6 winston: ^3.2.1 @@ -4549,7 +4680,7 @@ __metadata: cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4572,7 +4703,7 @@ __metadata: "@types/supertest": ^2.0.8 express: ^4.17.1 express-promise-router: ^4.1.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 winston: ^3.2.1 yn: ^4.0.0 @@ -4610,7 +4741,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4661,7 +4792,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4674,6 +4805,7 @@ __metadata: resolution: "@backstage/plugin-bazaar-backend@workspace:plugins/bazaar-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -4726,7 +4858,7 @@ __metadata: "@backstage/integration": "workspace:^" "@openapitools/openapi-generator-cli": ^2.4.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 ts-morph: ^17.0.0 languageName: unknown linkType: soft @@ -4755,7 +4887,7 @@ __metadata: cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.9.6 react-use: ^17.2.4 recharts: ^2.0.0 @@ -4778,7 +4910,9 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/types": "workspace:^" "@types/lodash": ^4.14.151 aws-sdk: ^2.840.0 @@ -4811,7 +4945,7 @@ __metadata: "@types/lodash": ^4.14.151 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -4837,7 +4971,7 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-events-node": "workspace:^" luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 uuid: ^8.0.0 winston: ^3.2.1 languageName: unknown @@ -4860,7 +4994,7 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" "@types/node-fetch": ^2.5.12 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -4883,7 +5017,7 @@ __metadata: "@backstage/types": "workspace:^" "@types/lodash": ^4.14.151 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 winston: ^3.2.1 languageName: unknown @@ -4907,7 +5041,7 @@ __metadata: "@types/fs-extra": ^9.0.1 fs-extra: 10.1.0 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -4940,7 +5074,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 minimatch: ^5.1.2 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -4967,7 +5101,7 @@ __metadata: "@types/uuid": ^8.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -4980,6 +5114,7 @@ __metadata: dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -4989,6 +5124,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@types/express": ^4.17.6 "@types/luxon": ^3.0.0 @@ -5041,7 +5177,7 @@ __metadata: "@types/node-fetch": ^2.5.12 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 p-limit: ^3.0.2 qs: ^6.9.4 @@ -5111,7 +5247,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 minimatch: ^5.0.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 p-limit: ^3.0.2 prom-client: ^14.0.1 @@ -5125,7 +5261,18 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-common@^1.0.8, @backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": +"@backstage/plugin-catalog-common@npm:^1.0.11, @backstage/plugin-catalog-common@npm:^1.0.8": + version: 1.0.11 + resolution: "@backstage/plugin-catalog-common@npm:1.0.11" + dependencies: + "@backstage/catalog-model": ^1.2.0 + "@backstage/plugin-permission-common": ^0.7.3 + "@backstage/plugin-search-common": ^1.2.1 + checksum: e0e37a96b4d97230c590fc4f1e17f252167a3cbbc79533c7e29a26d3480e82435c57ae6f7952aaf694bd8dc22245410c9fd29853249b1b73ed3a009dd49bce6b + languageName: node + linkType: hard + +"@backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-common@workspace:plugins/catalog-common" dependencies: @@ -5180,17 +5327,17 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" - "@graphql-codegen/cli": ^2.3.1 - "@graphql-codegen/graphql-modules-preset": ^2.3.2 - "@graphql-codegen/typescript": ^2.4.2 - "@graphql-codegen/typescript-resolvers": ^2.4.3 + "@graphql-codegen/cli": ^3.0.0 + "@graphql-codegen/graphql-modules-preset": ^3.0.0 + "@graphql-codegen/typescript": ^3.0.0 + "@graphql-codegen/typescript-resolvers": ^3.0.0 "@graphql-tools/schema": ^9.0.0 apollo-server: ^3.0.0 graphql: ^16.0.0 graphql-modules: ^2.0.0 graphql-tag: ^2.11.0 graphql-type-json: ^0.3.2 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 winston: ^3.2.1 languageName: unknown @@ -5226,7 +5373,7 @@ __metadata: git-url-parse: ^13.0.0 js-base64: ^3.6.0 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 react-hook-form: ^7.12.2 react-use: ^17.2.4 yaml: ^2.0.0 @@ -5253,7 +5400,42 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-react@^1.2.1, @backstage/plugin-catalog-react@^1.2.3, @backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": +"@backstage/plugin-catalog-react@npm:^1.2.1, @backstage/plugin-catalog-react@npm:^1.2.4, @backstage/plugin-catalog-react@npm:^1.3.0": + version: 1.3.0 + resolution: "@backstage/plugin-catalog-react@npm:1.3.0" + dependencies: + "@backstage/catalog-client": ^1.3.1 + "@backstage/catalog-model": ^1.2.0 + "@backstage/core-components": ^0.12.4 + "@backstage/core-plugin-api": ^1.4.0 + "@backstage/errors": ^1.1.4 + "@backstage/integration": ^1.4.2 + "@backstage/plugin-catalog-common": ^1.0.11 + "@backstage/plugin-permission-common": ^0.7.3 + "@backstage/plugin-permission-react": ^0.4.10 + "@backstage/theme": ^0.2.17 + "@backstage/types": ^1.0.2 + "@backstage/version-bridge": ^1.0.3 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + classnames: ^2.2.6 + jwt-decode: ^3.1.0 + lodash: ^4.17.21 + material-ui-popup-state: ^1.9.3 + qs: ^6.9.4 + react-use: ^17.2.4 + yaml: ^2.0.0 + zen-observable: ^0.10.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 03cd5a270154c0cde63bd33104d11ba5811690205c697441f207606d961935f5c45ad2a8e17f591c5ac8b294601c1855852bac35ccb30125e2ab6df13daf82d2 + languageName: node + linkType: hard + +"@backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-react@workspace:plugins/catalog-react" dependencies: @@ -5364,7 +5546,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@date-io/luxon": ^1.3.13 - "@material-ui/core": ^4.9.13 + "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 "@material-ui/pickers": ^3.3.10 @@ -5407,7 +5589,7 @@ __metadata: humanize-duration: ^3.27.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5437,7 +5619,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.9.4 react-use: ^17.2.4 peerDependencies: @@ -5470,7 +5652,7 @@ __metadata: cross-fetch: ^3.1.5 humanize-duration: ^3.27.1 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5496,7 +5678,7 @@ __metadata: express-promise-router: ^4.1.0 express-xml-bodyparser: ^0.3.0 knex: ^2.0.0 - msw: ^0.49.0 + msw: ^1.0.0 supertest: ^6.1.6 uuid: ^8.3.2 winston: ^3.2.1 @@ -5533,7 +5715,7 @@ __metadata: cross-fetch: ^3.1.5 highlight.js: ^10.6.0 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 recharts: ^2.0.0 peerDependencies: @@ -5555,7 +5737,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@material-ui/core": ^4.9.10 + "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": ^4.0.0-alpha.57 "@testing-library/jest-dom": ^5.10.1 @@ -5563,7 +5745,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": "*" cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 rc-progress: 3.4.1 react-use: ^17.2.4 peerDependencies: @@ -5595,7 +5777,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 jsonschema: ^1.2.6 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 zen-observable: ^0.10.0 peerDependencies: @@ -5643,7 +5825,7 @@ __metadata: cross-fetch: ^3.1.5 history: ^5.0.0 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 pluralize: ^8.0.0 qs: ^6.9.4 react-use: ^17.2.4 @@ -5669,7 +5851,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@material-ui/core": ^4.9.13 + "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 "@testing-library/jest-dom": ^5.10.1 @@ -5677,7 +5859,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": "*" express: ^4.18.1 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: "@backstage/plugin-catalog-react": "workspace:^" @@ -5685,6 +5867,110 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-entity-feedback-backend@workspace:^, @backstage/plugin-entity-feedback-backend@workspace:plugins/entity-feedback-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-entity-feedback-backend@workspace:plugins/entity-feedback-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-entity-feedback-common": "workspace:^" + "@types/express": "*" + "@types/supertest": ^2.0.12 + express: ^4.18.1 + express-promise-router: ^4.1.0 + knex: ^2.0.0 + msw: ^1.0.0 + node-fetch: ^2.6.7 + supertest: ^6.2.4 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-entity-feedback-common@workspace:^, @backstage/plugin-entity-feedback-common@workspace:plugins/entity-feedback-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-entity-feedback-common@workspace:plugins/entity-feedback-common" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/plugin-entity-feedback@workspace:^, @backstage/plugin-entity-feedback@workspace:plugins/entity-feedback": + version: 0.0.0-use.local + resolution: "@backstage/plugin-entity-feedback@workspace:plugins/entity-feedback" + dependencies: + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-entity-feedback-common": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/react-hooks": ^8.0.1 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + msw: ^1.0.0 + react-use: ^17.2.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-entity-validation@workspace:plugins/entity-validation": + version: 0.0.0-use.local + resolution: "@backstage/plugin-entity-validation@workspace:plugins/entity-validation" + dependencies: + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@codemirror/language": ^6.0.0 + "@codemirror/legacy-modes": ^6.1.0 + "@codemirror/view": ^6.0.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@react-hookz/web": ^20.0.0 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + "@uiw/react-codemirror": ^4.9.3 + cross-fetch: ^3.1.5 + lodash: ^4.17.21 + msw: ^1.0.0 + react-use: ^17.2.4 + yaml: ^2.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-events-backend-module-aws-sqs@workspace:plugins/events-backend-module-aws-sqs": version: 0.0.0-use.local resolution: "@backstage/plugin-events-backend-module-aws-sqs@workspace:plugins/events-backend-module-aws-sqs" @@ -5830,7 +6116,7 @@ __metadata: express: ^4.18.1 express-promise-router: ^4.1.0 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 winston: ^3.2.1 @@ -5860,7 +6146,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 languageName: unknown linkType: soft @@ -5892,7 +6178,7 @@ __metadata: "@types/node": ^16.11.26 classnames: ^2.2.6 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 @@ -5922,7 +6208,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5952,7 +6238,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 p-limit: ^3.0.2 react-use: ^17.2.4 peerDependencies: @@ -5989,7 +6275,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 material-ui-popup-state: ^1.9.3 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6016,7 +6302,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 @@ -6047,7 +6333,7 @@ __metadata: "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.10.1 react-use: ^17.2.4 recharts: ^2.0.0 @@ -6082,7 +6368,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6116,7 +6402,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6149,7 +6435,7 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 octokit: ^2.0.4 react-use: ^17.4.0 peerDependencies: @@ -6181,7 +6467,7 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 p-limit: ^4.0.0 react-use: ^17.2.4 peerDependencies: @@ -6210,7 +6496,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6244,7 +6530,7 @@ __metadata: cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.10.1 react-use: ^17.2.4 peerDependencies: @@ -6275,7 +6561,7 @@ __metadata: graphiql: ^1.5.12 graphql: ^16.0.0 graphql-ws: ^5.4.1 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6301,7 +6587,7 @@ __metadata: graphql: ^16.0.0 graphql-modules: ^2.0.0 helmet: ^6.0.0 - msw: ^0.49.0 + msw: ^1.0.0 reflect-metadata: ^0.1.13 supertest: ^6.1.3 winston: ^3.2.1 @@ -6309,7 +6595,57 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-home@^0.4.29, @backstage/plugin-home@workspace:^, @backstage/plugin-home@workspace:plugins/home": +"@backstage/plugin-graphql-voyager@workspace:plugins/graphql-voyager": + version: 0.0.0-use.local + resolution: "@backstage/plugin-graphql-voyager@workspace:plugins/graphql-voyager" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + graphql-voyager: ^1.0.0-rc.31 + msw: ^1.0.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-home@npm:^0.4.30": + version: 0.4.31 + resolution: "@backstage/plugin-home@npm:0.4.31" + dependencies: + "@backstage/catalog-model": ^1.2.0 + "@backstage/config": ^1.0.6 + "@backstage/core-components": ^0.12.4 + "@backstage/core-plugin-api": ^1.4.0 + "@backstage/plugin-catalog-react": ^1.3.0 + "@backstage/theme": ^0.2.17 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + lodash: ^4.17.21 + react-use: ^17.2.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 2ba5d377713540a4f9ab7083bc1595361678b59c80d57f26f2fe0bd660ec77926a6bfdeb52a2c6179e809e5186360333ae1bb41f51e741c4bdb7fc3edfda60c3 + languageName: node + linkType: hard + +"@backstage/plugin-home@workspace:^, @backstage/plugin-home@workspace:plugins/home": version: 0.0.0-use.local resolution: "@backstage/plugin-home@workspace:plugins/home" dependencies: @@ -6321,7 +6657,6 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" - "@backstage/plugin-stack-overflow": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@material-ui/core": ^4.12.2 @@ -6333,7 +6668,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 @@ -6368,7 +6703,7 @@ __metadata: cross-fetch: ^3.1.5 humanize-duration: ^3.26.0 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6394,7 +6729,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 jenkins: ^1.0.0 - msw: ^0.49.0 + msw: ^1.0.0 supertest: ^6.1.6 winston: ^3.2.1 yn: ^4.0.0 @@ -6436,7 +6771,7 @@ __metadata: "@types/testing-library__jest-dom": ^5.9.1 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6449,6 +6784,7 @@ __metadata: resolution: "@backstage/plugin-kafka-backend@workspace:plugins/kafka-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -6490,7 +6826,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 jest-when: ^3.1.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6514,7 +6850,7 @@ __metadata: "@backstage/plugin-kubernetes-common": "workspace:^" "@google-cloud/container": ^4.0.0 "@jest-mock/express": ^2.0.1 - "@kubernetes/client-node": 0.18.0 + "@kubernetes/client-node": 0.18.1 "@types/aws4": ^1.5.1 "@types/express": ^4.17.6 "@types/http-proxy-middleware": ^0.19.3 @@ -6533,7 +6869,7 @@ __metadata: luxon: ^3.0.0 mock-fs: ^5.2.0 morgan: ^1.10.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 stream-buffers: ^3.0.2 supertest: ^6.1.3 @@ -6548,7 +6884,7 @@ __metadata: dependencies: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" - "@kubernetes/client-node": 0.18.0 + "@kubernetes/client-node": 0.18.1 languageName: unknown linkType: soft @@ -6567,7 +6903,7 @@ __metadata: "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@kubernetes/client-node": 0.18.0 + "@kubernetes/client-node": 0.18.1 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 @@ -6582,7 +6918,7 @@ __metadata: js-yaml: ^4.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6590,6 +6926,31 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-lighthouse-backend@workspace:^, @backstage/plugin-lighthouse-backend@workspace:plugins/lighthouse-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-lighthouse-backend@workspace:plugins/lighthouse-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-lighthouse-common": "workspace:^" + "@backstage/types": "workspace:^" + winston: ^3.2.1 + languageName: unknown + linkType: soft + +"@backstage/plugin-lighthouse-common@workspace:^, @backstage/plugin-lighthouse-common@workspace:plugins/lighthouse-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-lighthouse-common@workspace:plugins/lighthouse-common" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-lighthouse@workspace:^, @backstage/plugin-lighthouse@workspace:plugins/lighthouse": version: 0.0.0-use.local resolution: "@backstage/plugin-lighthouse@workspace:plugins/lighthouse" @@ -6602,6 +6963,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-lighthouse-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@material-ui/core": ^4.12.2 @@ -6614,7 +6976,7 @@ __metadata: "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6622,6 +6984,110 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-linguist-backend@workspace:^, @backstage/plugin-linguist-backend@workspace:plugins/linguist-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-linguist-backend@workspace:plugins/linguist-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-linguist-common": "workspace:^" + "@backstage/types": "workspace:^" + "@types/express": "*" + "@types/supertest": ^2.0.8 + express: ^4.18.1 + express-promise-router: ^4.1.0 + fs-extra: ^10.0.0 + knex: ^2.0.0 + linguist-js: ^2.5.3 + luxon: ^2.0.2 + msw: ^1.0.0 + node-fetch: ^2.6.7 + supertest: ^6.2.4 + uuid: ^8.3.2 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-linguist-common@workspace:^, @backstage/plugin-linguist-common@workspace:plugins/linguist-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-linguist-common@workspace:plugins/linguist-common" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/plugin-linguist@workspace:^, @backstage/plugin-linguist@workspace:plugins/linguist": + version: 0.0.0-use.local + resolution: "@backstage/plugin-linguist@workspace:plugins/linguist" + dependencies: + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-linguist-common": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + luxon: ^2.0.2 + msw: ^1.0.0 + react-use: ^17.2.4 + slugify: ^1.6.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-microsoft-calendar@workspace:^, @backstage/plugin-microsoft-calendar@workspace:plugins/microsoft-calendar": + version: 0.0.0-use.local + resolution: "@backstage/plugin-microsoft-calendar@workspace:plugins/microsoft-calendar" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@microsoft/microsoft-graph-types": ^2.25.0 + "@tanstack/react-query": ^4.1.3 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": ^16.11.26 + classnames: ^2.3.1 + cross-fetch: ^3.1.5 + dompurify: ^2.3.6 + lodash: ^4.17.21 + luxon: ^3.0.0 + material-ui-popup-state: ^1.9.3 + msw: ^1.0.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-newrelic-dashboard@workspace:^, @backstage/plugin-newrelic-dashboard@workspace:plugins/newrelic-dashboard": version: 0.0.0-use.local resolution: "@backstage/plugin-newrelic-dashboard@workspace:plugins/newrelic-dashboard" @@ -6664,7 +7130,35 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-octopus-deploy@workspace:plugins/octopus-deploy": + version: 0.0.0-use.local + resolution: "@backstage/plugin-octopus-deploy@workspace:plugins/octopus-deploy" + dependencies: + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6685,7 +7179,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@material-ui/core": ^4.9.13 + "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": ^4.0.0-alpha.57 "@testing-library/jest-dom": ^5.10.1 @@ -6693,7 +7187,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": "*" cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6722,7 +7216,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 p-limit: ^3.1.0 pluralize: ^8.0.0 qs: ^6.10.1 @@ -6757,7 +7251,7 @@ __metadata: classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 @@ -6771,13 +7265,14 @@ __metadata: resolution: "@backstage/plugin-periskop-backend@workspace:plugins/periskop-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@types/express": "*" "@types/supertest": ^2.0.8 express: ^4.17.1 express-promise-router: ^4.1.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.1.6 winston: ^3.2.1 @@ -6809,7 +7304,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6835,7 +7330,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.1.6 winston: ^3.2.1 @@ -6844,7 +7339,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-permission-common@workspace:^, @backstage/plugin-permission-common@workspace:plugins/permission-common": +"@backstage/plugin-permission-common@^0.7.3, @backstage/plugin-permission-common@workspace:^, @backstage/plugin-permission-common@workspace:plugins/permission-common": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-common@workspace:plugins/permission-common" dependencies: @@ -6853,7 +7348,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/types": "workspace:^" cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 uuid: ^8.0.0 zod: ~3.18.0 languageName: unknown @@ -6874,13 +7369,31 @@ __metadata: "@types/supertest": ^2.0.8 express: ^4.17.1 express-promise-router: ^4.1.0 - msw: ^0.49.0 + msw: ^1.0.0 supertest: ^6.1.3 zod: ~3.18.0 zod-to-json-schema: ~3.18.0 languageName: unknown linkType: soft +"@backstage/plugin-permission-react@npm:^0.4.10": + version: 0.4.10 + resolution: "@backstage/plugin-permission-react@npm:0.4.10" + dependencies: + "@backstage/config": ^1.0.6 + "@backstage/core-plugin-api": ^1.4.0 + "@backstage/plugin-permission-common": ^0.7.3 + cross-fetch: ^3.1.5 + react-use: ^17.2.4 + swr: ^2.0.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: f7a82648a31a8fe9873492abdbcc524549205e3fd11fe144ab225e6263ce2b2e46abd642e6c370d692d424c51e7c55b9259d8178500f9bfbfc9267d09596843c + languageName: node + linkType: hard + "@backstage/plugin-permission-react@workspace:^, @backstage/plugin-permission-react@workspace:plugins/permission-react": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-react@workspace:plugins/permission-react" @@ -6922,7 +7435,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 knex: ^2.0.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.1.3 uuid: ^8.2.0 @@ -6960,7 +7473,7 @@ __metadata: "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@material-ui/core": ^4.9.13 + "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": ^4.0.0-alpha.57 "@testing-library/jest-dom": ^5.10.1 @@ -6970,7 +7483,7 @@ __metadata: "@types/node": "*" cross-fetch: ^3.1.5 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.9.4 react-hook-form: ^7.13.0 react-use: ^17.2.4 @@ -6986,6 +7499,7 @@ __metadata: resolution: "@backstage/plugin-proxy-backend@workspace:plugins/proxy-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@types/express": ^4.17.6 @@ -6997,7 +7511,7 @@ __metadata: express-promise-router: ^4.1.0 http-proxy-middleware: ^2.0.0 morgan: ^1.10.0 - msw: ^0.49.0 + msw: ^1.0.0 supertest: ^6.1.3 uuid: ^8.0.0 winston: ^3.2.1 @@ -7025,7 +7539,7 @@ __metadata: fs-extra: 10.1.0 lodash: ^4.17.21 morgan: ^1.10.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.1.3 winston: ^3.2.1 @@ -7057,7 +7571,7 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 react-sparklines: ^1.7.0 react-use: ^17.2.4 peerDependencies: @@ -7076,6 +7590,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 @@ -7083,7 +7598,7 @@ __metadata: command-exists: ^1.2.9 fs-extra: 10.1.0 mock-fs: ^5.1.0 - msw: ^0.49.0 + msw: ^1.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -7099,6 +7614,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 @@ -7117,20 +7633,8 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" - "@backstage/core-app-api": "workspace:^" - "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" - "@backstage/integration": "workspace:^" - "@backstage/plugin-scaffolder-backend": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@testing-library/jest-dom": ^5.10.1 - "@testing-library/react": ^12.1.3 - "@testing-library/user-event": ^14.0.0 - "@types/node": "*" - cross-fetch: ^3.1.5 - msw: ^0.49.0 - peerDependencies: - react: ^16.13.1 || ^17.0.0 + "@backstage/plugin-scaffolder-node": "workspace:^" languageName: unknown linkType: soft @@ -7141,7 +7645,7 @@ __metadata: "@backstage/backend-common": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" - "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" winston: ^3.2.1 yeoman-environment: ^3.9.1 @@ -7166,6 +7670,7 @@ __metadata: "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" "@gitbeaker/core": ^35.6.0 "@gitbeaker/node": ^35.1.0 @@ -7182,7 +7687,7 @@ __metadata: command-exists: ^1.2.9 compression: ^1.7.4 cors: ^2.8.5 - esbuild: ^0.16.0 + esbuild: ^0.17.0 express: ^4.17.1 express-promise-router: ^4.1.0 fs-extra: 10.1.0 @@ -7197,7 +7702,7 @@ __metadata: luxon: ^3.0.0 mock-fs: ^5.1.0 morgan: ^1.10.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 nunjucks: ^3.2.3 octokit: ^2.0.0 @@ -7225,6 +7730,20 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-scaffolder-node@workspace:^, @backstage/plugin-scaffolder-node@workspace:plugins/scaffolder-node": + version: 0.0.0-use.local + resolution: "@backstage/plugin-scaffolder-node@workspace:plugins/scaffolder-node" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-scaffolder-common": "workspace:^" + "@backstage/types": "workspace:^" + jsonschema: ^1.2.6 + winston: ^3.2.1 + languageName: unknown + linkType: soft + "@backstage/plugin-scaffolder-react@workspace:^, @backstage/plugin-scaffolder-react@workspace:plugins/scaffolder-react": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-react@workspace:plugins/scaffolder-react" @@ -7247,23 +7766,30 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 + "@react-hookz/web": ^20.0.0 "@rjsf/core": ^3.2.1 - "@rjsf/core-v5": "npm:@rjsf/core@5.0.0-beta.16" + "@rjsf/core-v5": "npm:@rjsf/core@5.1.0" "@rjsf/material-ui": ^3.2.1 - "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.0.0-beta.16" - "@rjsf/utils": 5.0.0-beta.16 - "@rjsf/validator-ajv6": 5.0.0-beta.16 + "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.1.0" + "@rjsf/utils": 5.1.0 + "@rjsf/validator-ajv8": 5.1.0 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 + "@types/humanize-duration": ^3.18.1 "@types/json-schema": ^7.0.9 + "@types/luxon": ^3.0.0 classnames: ^2.2.6 + humanize-duration: ^3.25.1 + immer: ^9.0.1 json-schema: ^0.4.0 json-schema-library: ^7.3.9 lodash: ^4.17.21 + luxon: ^3.0.0 qs: ^6.9.4 react-use: ^17.2.4 + use-immer: ^0.8.0 zen-observable: ^0.10.0 zod: ~3.18.0 zod-to-json-schema: ~3.18.0 @@ -7306,9 +7832,9 @@ __metadata: "@material-ui/lab": 4.0.0-alpha.57 "@react-hookz/web": ^20.0.0 "@rjsf/core": ^3.2.1 - "@rjsf/core-v5": "npm:@rjsf/core@5.0.0-beta.16" "@rjsf/material-ui": ^3.2.1 - "@rjsf/utils": 5.0.0-beta.16 + "@rjsf/utils": 5.1.0 + "@rjsf/validator-ajv8": 5.1.0 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 @@ -7327,10 +7853,9 @@ __metadata: json-schema-library: ^7.3.9 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.9.4 react-use: ^17.2.4 - use-immer: ^0.8.0 yaml: ^2.0.0 zen-observable: ^0.10.0 zod: ~3.18.0 @@ -7430,7 +7955,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-search-common@workspace:^, @backstage/plugin-search-common@workspace:plugins/search-common": +"@backstage/plugin-search-common@^1.2.1, @backstage/plugin-search-common@workspace:^, @backstage/plugin-search-common@workspace:plugins/search-common": version: 0.0.0-use.local resolution: "@backstage/plugin-search-common@workspace:plugins/search-common" dependencies: @@ -7498,7 +8023,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.9.4 react-use: ^17.2.4 peerDependencies: @@ -7533,7 +8058,7 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-sparklines: ^1.7.0 react-use: ^17.2.4 peerDependencies: @@ -7563,7 +8088,7 @@ __metadata: "@types/node": ^16.11.26 "@types/zen-observable": ^0.8.2 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-hook-form: ^7.12.2 react-use: ^17.2.4 uuid: ^8.3.2 @@ -7587,7 +8112,7 @@ __metadata: "@types/supertest": ^2.0.12 express: ^4.18.1 express-promise-router: ^4.1.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 winston: ^3.2.1 @@ -7630,7 +8155,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 rc-progress: 3.4.1 react-use: ^17.2.4 peerDependencies: @@ -7662,7 +8187,7 @@ __metadata: classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -7680,7 +8205,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 qs: ^6.9.4 winston: ^3.2.1 @@ -7710,7 +8235,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.9.4 react-use: ^17.2.4 peerDependencies: @@ -7719,6 +8244,33 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-stackstorm@workspace:^, @backstage/plugin-stackstorm@workspace:plugins/stackstorm": + version: 0.0.0-use.local + resolution: "@backstage/plugin-stackstorm@workspace:plugins/stackstorm" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + msw: ^1.0.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-tech-insights-backend-module-jsonfc@workspace:^, @backstage/plugin-tech-insights-backend-module-jsonfc@workspace:plugins/tech-insights-backend-module-jsonfc": version: 0.0.0-use.local resolution: "@backstage/plugin-tech-insights-backend-module-jsonfc@workspace:plugins/tech-insights-backend-module-jsonfc" @@ -7821,7 +8373,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 qs: ^6.9.4 react-use: ^17.2.4 peerDependencies: @@ -7855,7 +8407,7 @@ __metadata: color: ^4.0.1 cross-fetch: ^3.1.5 d3-force: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 prop-types: ^15.7.2 react-use: ^17.2.4 peerDependencies: @@ -7879,7 +8431,7 @@ __metadata: "@backstage/plugin-techdocs-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@material-ui/core": ^4.9.13 + "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 "@testing-library/jest-dom": ^5.10.1 @@ -7887,7 +8439,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 testing-library__dom: ^7.29.4-beta.1 peerDependencies: @@ -7923,7 +8475,7 @@ __metadata: fs-extra: 10.1.0 knex: ^2.0.0 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 p-limit: ^3.1.0 supertest: ^6.1.3 @@ -7946,7 +8498,7 @@ __metadata: "@backstage/plugin-techdocs-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@material-ui/core": ^4.9.13 + "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 "@react-hookz/web": ^20.0.0 @@ -7957,7 +8509,8 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 git-url-parse: ^13.0.0 - msw: ^0.49.0 + msw: ^1.0.0 + photoswipe: ^5.3.5 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -7971,6 +8524,7 @@ __metadata: "@aws-sdk/client-s3": ^3.208.0 "@aws-sdk/credential-providers": ^3.208.0 "@aws-sdk/lib-storage": ^3.208.0 + "@aws-sdk/node-http-handler": ^3.208.0 "@aws-sdk/types": ^3.208.0 "@azure/identity": ^2.1.0 "@azure/storage-blob": ^12.5.0 @@ -7995,6 +8549,7 @@ __metadata: express: ^4.17.1 fs-extra: 10.1.0 git-url-parse: ^13.0.0 + hpagent: ^1.2.0 js-yaml: ^4.0.0 json5: ^2.1.3 mime-types: ^2.1.27 @@ -8024,7 +8579,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 - jss: ~10.9.0 + jss: ~10.10.0 lodash: ^4.17.21 react-helmet: 6.1.0 react-use: ^17.2.4 @@ -8072,9 +8627,9 @@ __metadata: dompurify: ^2.2.9 event-source-polyfill: 1.0.25 git-url-parse: ^13.0.0 - jss: ~10.9.0 + jss: ~10.10.0 lodash: ^4.17.21 - msw: ^0.49.0 + msw: ^1.0.0 react-helmet: 6.1.0 react-use: ^17.2.4 peerDependencies: @@ -8090,18 +8645,20 @@ __metadata: resolution: "@backstage/plugin-todo-backend@workspace:plugins/todo-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" "@types/express": ^4.17.6 "@types/supertest": ^2.0.8 express: ^4.17.1 express-promise-router: ^4.1.0 leasot: ^12.0.0 - msw: ^0.49.0 + msw: ^1.0.0 supertest: ^6.1.3 winston: ^3.2.1 yn: ^4.0.0 @@ -8130,7 +8687,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -8143,6 +8700,7 @@ __metadata: resolution: "@backstage/plugin-user-settings-backend@workspace:plugins/user-settings-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" @@ -8183,7 +8741,7 @@ __metadata: "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 zen-observable: ^0.10.0 peerDependencies: @@ -8210,7 +8768,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 helmet: ^6.0.0 - msw: ^0.49.0 + msw: ^1.0.0 node-fetch: ^2.6.7 p-limit: ^3.1.0 supertest: ^6.1.6 @@ -8241,7 +8799,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": "*" cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -8271,7 +8829,7 @@ __metadata: cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 recharts: ^2.0.0 peerDependencies: @@ -8287,7 +8845,7 @@ __metadata: "@backstage/test-utils": "workspace:^" "@types/node": ^16.0.0 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 languageName: unknown linkType: soft @@ -8298,6 +8856,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/types": "workspace:^" "@manypkg/get-packages": ^1.1.3 "@microsoft/api-documenter": ^7.19.27 "@microsoft/api-extractor": ^7.33.7 @@ -8344,7 +8903,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 zen-observable: ^0.10.0 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 @@ -8354,7 +8913,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/theme@^0.2.16, @backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": +"@backstage/theme@^0.2.16, @backstage/theme@^0.2.17, @backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": version: 0.0.0-use.local resolution: "@backstage/theme@workspace:packages/theme" dependencies: @@ -8363,7 +8922,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/types@^1.0.1, @backstage/types@workspace:^, @backstage/types@workspace:packages/types": +"@backstage/types@^1.0.1, @backstage/types@^1.0.2, @backstage/types@workspace:^, @backstage/types@workspace:packages/types": version: 0.0.0-use.local resolution: "@backstage/types@workspace:packages/types" dependencies: @@ -8374,7 +8933,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/version-bridge@^1.0.2, @backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": +"@backstage/version-bridge@^1.0.2, @backstage/version-bridge@^1.0.3, @backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": version: 0.0.0-use.local resolution: "@backstage/version-bridge@workspace:packages/version-bridge" dependencies: @@ -8675,8 +9234,8 @@ __metadata: linkType: hard "@codemirror/language@npm:^6.0.0": - version: 6.3.2 - resolution: "@codemirror/language@npm:6.3.2" + version: 6.6.0 + resolution: "@codemirror/language@npm:6.6.0" dependencies: "@codemirror/state": ^6.0.0 "@codemirror/view": ^6.0.0 @@ -8684,7 +9243,7 @@ __metadata: "@lezer/highlight": ^1.0.0 "@lezer/lr": ^1.0.0 style-mod: ^4.0.0 - checksum: b70ed9b85d0bea79181c86e88a1f5c0bada30680ee1fe6a68efc01bc037c3d14f94a83602fc46cc4b4393589605ef7e986ed5174443502f3365dd61f883894fa + checksum: bb9411620e2f231653a3f0c4429e0d19a3843bff5dbc117df4649d7bf783ec4ad809c0add8bc0887a4ec3f48b4f8f941621168e47d76101d5383f0d670af1722 languageName: node linkType: hard @@ -8739,13 +9298,13 @@ __metadata: linkType: hard "@codemirror/view@npm:^6.0.0": - version: 6.7.2 - resolution: "@codemirror/view@npm:6.7.2" + version: 6.9.1 + resolution: "@codemirror/view@npm:6.9.1" dependencies: "@codemirror/state": ^6.1.4 style-mod: ^4.0.0 w3c-keyname: ^2.2.4 - checksum: ad08a4a0a50e2b34c0141311e7db9f045a2186b81ae28271318374b5b9964435267ec0af210b5d061cb84bc8fdca6dabc1919eb7fe05837d931038b9095c3f8c + checksum: 485d54d338a27ebde6bec489e3008db80a2a801fe27db0d4a3c70181c8739f806f035855ba13d5b311d186ad2b130db07196e8b465ec102b0142df0689bff2f8 languageName: node linkType: hard @@ -8920,9 +9479,36 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/android-arm64@npm:0.16.16" +"@esbuild-kit/cjs-loader@npm:^2.4.1": + version: 2.4.2 + resolution: "@esbuild-kit/cjs-loader@npm:2.4.2" + dependencies: + "@esbuild-kit/core-utils": ^3.0.0 + get-tsconfig: ^4.4.0 + checksum: e346e339bfc7eff5c52c270fd0ec06a7f2341b624adfb69f84b7d83f119c35070420906f2761a0b4604e0a0ec90e35eaf12544585476c428ed6d6ee3b250c0fe + languageName: node + linkType: hard + +"@esbuild-kit/core-utils@npm:^3.0.0": + version: 3.0.0 + resolution: "@esbuild-kit/core-utils@npm:3.0.0" + dependencies: + esbuild: ~0.15.10 + source-map-support: ^0.5.21 + checksum: 0e89ec718e2211bf95c48a8085aaef88e8e416f42abd1c62d488d5458eecd3fbc144179a0c5570ad36fa7e2d3bbc411f8d3fb28802c37ced2154dc2c6ded9dfe + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/android-arm64@npm:0.16.17" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/android-arm64@npm:0.17.10" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -8934,65 +9520,128 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/android-arm@npm:0.16.16" +"@esbuild/android-arm@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/android-arm@npm:0.16.17" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/android-x64@npm:0.16.16" +"@esbuild/android-arm@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/android-arm@npm:0.17.10" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/android-x64@npm:0.16.17" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/darwin-arm64@npm:0.16.16" +"@esbuild/android-x64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/android-x64@npm:0.17.10" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/darwin-arm64@npm:0.16.17" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/darwin-x64@npm:0.16.16" +"@esbuild/darwin-arm64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/darwin-arm64@npm:0.17.10" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/darwin-x64@npm:0.16.17" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/freebsd-arm64@npm:0.16.16" +"@esbuild/darwin-x64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/darwin-x64@npm:0.17.10" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/freebsd-arm64@npm:0.16.17" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/freebsd-x64@npm:0.16.16" +"@esbuild/freebsd-arm64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/freebsd-arm64@npm:0.17.10" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/freebsd-x64@npm:0.16.17" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/linux-arm64@npm:0.16.16" +"@esbuild/freebsd-x64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/freebsd-x64@npm:0.17.10" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/linux-arm64@npm:0.16.17" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/linux-arm@npm:0.16.16" +"@esbuild/linux-arm64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/linux-arm64@npm:0.17.10" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/linux-arm@npm:0.16.17" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/linux-ia32@npm:0.16.16" +"@esbuild/linux-arm@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/linux-arm@npm:0.17.10" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/linux-ia32@npm:0.16.17" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/linux-ia32@npm:0.17.10" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -9004,86 +9653,170 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/linux-loong64@npm:0.16.16" +"@esbuild/linux-loong64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/linux-loong64@npm:0.16.17" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/linux-mips64el@npm:0.16.16" +"@esbuild/linux-loong64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/linux-loong64@npm:0.17.10" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/linux-mips64el@npm:0.16.17" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/linux-ppc64@npm:0.16.16" +"@esbuild/linux-mips64el@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/linux-mips64el@npm:0.17.10" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/linux-ppc64@npm:0.16.17" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/linux-riscv64@npm:0.16.16" +"@esbuild/linux-ppc64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/linux-ppc64@npm:0.17.10" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/linux-riscv64@npm:0.16.17" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/linux-s390x@npm:0.16.16" +"@esbuild/linux-riscv64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/linux-riscv64@npm:0.17.10" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/linux-s390x@npm:0.16.17" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/linux-x64@npm:0.16.16" +"@esbuild/linux-s390x@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/linux-s390x@npm:0.17.10" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/linux-x64@npm:0.16.17" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/netbsd-x64@npm:0.16.16" +"@esbuild/linux-x64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/linux-x64@npm:0.17.10" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/netbsd-x64@npm:0.16.17" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/openbsd-x64@npm:0.16.16" +"@esbuild/netbsd-x64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/netbsd-x64@npm:0.17.10" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/openbsd-x64@npm:0.16.17" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/sunos-x64@npm:0.16.16" +"@esbuild/openbsd-x64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/openbsd-x64@npm:0.17.10" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/sunos-x64@npm:0.16.17" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/win32-arm64@npm:0.16.16" +"@esbuild/sunos-x64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/sunos-x64@npm:0.17.10" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/win32-arm64@npm:0.16.17" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/win32-ia32@npm:0.16.16" +"@esbuild/win32-arm64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/win32-arm64@npm:0.17.10" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/win32-ia32@npm:0.16.17" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.16.16": - version: 0.16.16 - resolution: "@esbuild/win32-x64@npm:0.16.16" +"@esbuild/win32-ia32@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/win32-ia32@npm:0.17.10" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.16.17": + version: 0.16.17 + resolution: "@esbuild/win32-x64@npm:0.16.17" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.17.10": + version: 0.17.10 + resolution: "@esbuild/win32-x64@npm:0.17.10" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -9105,6 +9838,56 @@ __metadata: languageName: node linkType: hard +"@f/animate@npm:^1.0.1": + version: 1.0.1 + resolution: "@f/animate@npm:1.0.1" + dependencies: + "@f/elapsed-time": ^1.0.0 + "@f/raf": ^1.0.0 + "@f/tween": ^1.0.0 + checksum: 660b44d297b1a6e5e28c48438633e00ded40f8e51adb4bfb26fc809b60c8c056f567f2e61f4f842c8fd2cf8a85dffaa1c2502bd0963eb926135f2d66d9f997db + languageName: node + linkType: hard + +"@f/elapsed-time@npm:^1.0.0": + version: 1.0.0 + resolution: "@f/elapsed-time@npm:1.0.0" + dependencies: + "@f/timestamp": ^1.0.0 + checksum: 5934d24a667bae4e0089408aa362c0ee1503945adf73ca0ff0d09fed33d62d070f0323eb9a54f42c37bd0545330a0f63da13319f66cc89fef59b8917a3278b5f + languageName: node + linkType: hard + +"@f/map-obj@npm:^1.2.2": + version: 1.2.2 + resolution: "@f/map-obj@npm:1.2.2" + checksum: d67db6074e1daa7a3c0f8c2e54094bff48b54d11817fddc886631dd8a17586d0040e7116282a374cc1d3d643d05862d49a1062ea5b2d03b9069507c09ae888a4 + languageName: node + linkType: hard + +"@f/raf@npm:^1.0.0": + version: 1.0.3 + resolution: "@f/raf@npm:1.0.3" + checksum: 800344caf4c649c0bcdefd4febbf3f0eed96bb9b7e54b5bc6eebd7d781c9e6d98ba22fb39da41474080d97ac21916bd1e82051558b43dabcbbf410d2dd0899c1 + languageName: node + linkType: hard + +"@f/timestamp@npm:^1.0.0": + version: 1.0.0 + resolution: "@f/timestamp@npm:1.0.0" + checksum: 8c37acb40cf67952309a214be2ddfd5c4f6634a414efc47406e8d1c1624e74662f584f6b48f3382c742f256aea1537e730e74c6078e52be3a28d0624cc7306a0 + languageName: node + linkType: hard + +"@f/tween@npm:^1.0.0": + version: 1.0.1 + resolution: "@f/tween@npm:1.0.1" + dependencies: + "@f/map-obj": ^1.2.2 + checksum: e37155c43875becc9eca8425b8dc83e64a78fa327738b33fcb3946418369568aa77007ade39591991379187381da829702e5cb84729dd2a0bc3bb4b7c94e4f2b + languageName: node + linkType: hard + "@fmvilas/pseudo-yaml-ast@npm:^0.3.1": version: 0.3.1 resolution: "@fmvilas/pseudo-yaml-ast@npm:0.3.1" @@ -9114,6 +9897,30 @@ __metadata: languageName: node linkType: hard +"@frsource/base64@npm:1.0.5": + version: 1.0.5 + resolution: "@frsource/base64@npm:1.0.5" + checksum: ba526dd81df4e91b0abbcc34fc6818c8ff132e5230c23a4448a365ba934f782a86abb2a8566050f9450905851e467a7160f01f3d321fbb0fa3c8c516c5a82b9b + languageName: node + linkType: hard + +"@frsource/cypress-plugin-visual-regression-diff@npm:^3.2.8": + version: 3.2.8 + resolution: "@frsource/cypress-plugin-visual-regression-diff@npm:3.2.8" + dependencies: + "@frsource/base64": 1.0.5 + glob: 8.0.3 + meta-png: 1.0.5 + move-file: 2.1.0 + pixelmatch: 5.3.0 + pngjs: 6.0.0 + sharp: 0.31.2 + peerDependencies: + cypress: ">=4.5.0" + checksum: 7e5a1daf3a36ec7f62ea643da8eff73aadb595e4fb8b400abec37e38a356a200de1ce47ff2e14666e71556300e4d044d5fa5ad21bdd2d214b93402addb1a490e + languageName: node + linkType: hard + "@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -9172,23 +9979,23 @@ __metadata: linkType: hard "@google-cloud/container@npm:^4.0.0": - version: 4.4.0 - resolution: "@google-cloud/container@npm:4.4.0" + version: 4.7.1 + resolution: "@google-cloud/container@npm:4.7.1" dependencies: google-gax: ^3.5.2 - checksum: 39e7ba3b8cd7e50908315eee5c42461b8cee909c5653ffc817503757e867d63815c5ca8f540b7566f38be9ae0454ed89d2efe95b64961a8692a9348ddf92074f + checksum: e622ddc2522fcffadd72a0cc6fa3f12e6a337b9e53ff1886a2057a8d4f3f0fc13b60b806c35946da0a1211fe52d66f65b4ab59c06b5e7cd725cd588477c0bde1 languageName: node linkType: hard "@google-cloud/firestore@npm:^6.0.0": - version: 6.4.2 - resolution: "@google-cloud/firestore@npm:6.4.2" + version: 6.4.3 + resolution: "@google-cloud/firestore@npm:6.4.3" dependencies: fast-deep-equal: ^3.1.1 functional-red-black-tree: ^1.0.1 - google-gax: ^3.5.2 + google-gax: ^3.5.3 protobufjs: ^7.0.0 - checksum: c259f59905d4939f0b1fff2fdfa3db38a0436454af9e4a8939ea951ee700d5f801375ed090e77f610fbf3f02db1aeba9d70437eb6a8ca6f0ba2088c8964c58d0 + checksum: 8bbde2c9bad94459c231d41855e65866b2383f20e2741aa93d86204370172dd62af94e1eb66d977e000e0c7292c986eb25fe967db7ee847593fd9a68c9fb91e3 languageName: node linkType: hard @@ -9217,8 +10024,8 @@ __metadata: linkType: hard "@google-cloud/storage@npm:^6.0.0": - version: 6.9.0 - resolution: "@google-cloud/storage@npm:6.9.0" + version: 6.9.3 + resolution: "@google-cloud/storage@npm:6.9.3" dependencies: "@google-cloud/paginator": ^3.0.7 "@google-cloud/projectify": ^3.0.0 @@ -9237,7 +10044,7 @@ __metadata: retry-request: ^5.0.0 teeny-request: ^8.0.0 uuid: ^8.0.0 - checksum: 886b9aad6bd44901b22a6e77c6430bae0effc2137e6c18196d151db8b9690d80726ecc4b60ac1ba315372305010282de6b9845ef23eaa7351449d0193aac62cc + checksum: 32bd3b49509b823504f0007d1d5cb1ec854bbc84e6be1ef397879b99c0f198c6251b85ddec81226428e046a06c0346356d82d26485e9d1f9e7ef943e091db06f languageName: node linkType: hard @@ -9274,151 +10081,152 @@ __metadata: languageName: node linkType: hard -"@graphql-codegen/cli@npm:^2.3.1": - version: 2.16.4 - resolution: "@graphql-codegen/cli@npm:2.16.4" +"@graphql-codegen/cli@npm:^3.0.0": + version: 3.2.1 + resolution: "@graphql-codegen/cli@npm:3.2.1" dependencies: "@babel/generator": ^7.18.13 "@babel/template": ^7.18.10 "@babel/types": ^7.18.13 - "@graphql-codegen/core": 2.6.8 - "@graphql-codegen/plugin-helpers": ^3.1.2 + "@graphql-codegen/core": ^3.1.0 + "@graphql-codegen/plugin-helpers": ^4.1.0 "@graphql-tools/apollo-engine-loader": ^7.3.6 - "@graphql-tools/code-file-loader": ^7.3.13 + "@graphql-tools/code-file-loader": ^7.3.17 "@graphql-tools/git-loader": ^7.2.13 "@graphql-tools/github-loader": ^7.3.20 "@graphql-tools/graphql-file-loader": ^7.5.0 "@graphql-tools/json-file-loader": ^7.4.1 - "@graphql-tools/load": 7.8.0 + "@graphql-tools/load": ^7.8.0 "@graphql-tools/prisma-loader": ^7.2.49 "@graphql-tools/url-loader": ^7.13.2 "@graphql-tools/utils": ^9.0.0 - "@whatwg-node/fetch": ^0.6.0 + "@parcel/watcher": ^2.1.0 + "@whatwg-node/fetch": ^0.8.0 chalk: ^4.1.0 - chokidar: ^3.5.2 cosmiconfig: ^7.0.0 - cosmiconfig-typescript-loader: 4.3.0 + cosmiconfig-typescript-loader: ^4.3.0 debounce: ^1.2.0 detect-indent: ^6.0.0 - graphql-config: 4.4.0 + graphql-config: ^4.4.0 inquirer: ^8.0.0 is-glob: ^4.0.1 json-to-pretty-yaml: ^1.2.2 listr2: ^4.0.5 log-symbols: ^4.0.0 + micromatch: ^4.0.5 shell-quote: ^1.7.3 string-env-interpolation: ^1.0.1 ts-log: ^2.2.3 + ts-node: ^10.9.1 tslib: ^2.4.0 yaml: ^1.10.0 yargs: ^17.0.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - ts-node: ">=10" bin: gql-gen: cjs/bin.js graphql-code-generator: cjs/bin.js graphql-codegen: cjs/bin.js graphql-codegen-esm: esm/bin.js - checksum: 6a6d99ac0220e993d71f30858b2b819bbc51685e7b376262493eff916e19be26d031f6859e71a8050c26cc69899bd2c5c0b797de09c45ae0312fd52bca2d8e65 + checksum: b62e2a3cee866cb006163946272f25ba9cc59bb2d45b7b3df5d1b220096e79183929456a440e5c4980d6a5664d6b14d7870d7f1760eb250b9211f4743d2afad3 languageName: node linkType: hard -"@graphql-codegen/core@npm:2.6.8": - version: 2.6.8 - resolution: "@graphql-codegen/core@npm:2.6.8" +"@graphql-codegen/core@npm:^3.1.0": + version: 3.1.0 + resolution: "@graphql-codegen/core@npm:3.1.0" dependencies: - "@graphql-codegen/plugin-helpers": ^3.1.1 + "@graphql-codegen/plugin-helpers": ^4.1.0 "@graphql-tools/schema": ^9.0.0 "@graphql-tools/utils": ^9.1.1 - tslib: ~2.4.0 + tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 33a222798fd99adcaf5d6d48fcd6949798a62d7a25e9b2af5b13e4def3de4338e5a743e5ea87661d2b32ae3279e3ad8b555d0e212efe86018088cb85a7d59d6a + checksum: 7ace2b185242d00575b877b0bbb884c66915d246ba17a98c0ab82c2ee3a93ff2c9ae9d7d75a1d346de02ca8f12eb02801ffe2da4846da6e164b43176cd02dd8c languageName: node linkType: hard -"@graphql-codegen/graphql-modules-preset@npm:^2.3.2": - version: 2.5.11 - resolution: "@graphql-codegen/graphql-modules-preset@npm:2.5.11" +"@graphql-codegen/graphql-modules-preset@npm:^3.0.0": + version: 3.1.0 + resolution: "@graphql-codegen/graphql-modules-preset@npm:3.1.0" dependencies: - "@graphql-codegen/plugin-helpers": ^3.1.2 - "@graphql-codegen/visitor-plugin-common": 2.13.7 + "@graphql-codegen/plugin-helpers": ^4.1.0 + "@graphql-codegen/visitor-plugin-common": 3.0.1 "@graphql-tools/utils": ^9.0.0 change-case-all: 1.0.15 parse-filepath: ^1.0.2 - tslib: ~2.4.0 + tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: c77f05964ff1253ee522ee43ef19e939933b3162150da0d6c0c9fb243020f058af1146160aba307427f0ecf6dfafaa16935fc8fe57cb74f94fb784671bb31f6b + checksum: cee2e7f8afbc0b0d81592abfc8894422b791f0eca6e878a789de980bf1710a3c726786ade296e653cc83f920b2618aa9f5c9dbb6934ee9da0122b2d10ef5bdb5 languageName: node linkType: hard -"@graphql-codegen/plugin-helpers@npm:^3.1.1, @graphql-codegen/plugin-helpers@npm:^3.1.2": - version: 3.1.2 - resolution: "@graphql-codegen/plugin-helpers@npm:3.1.2" +"@graphql-codegen/plugin-helpers@npm:^4.1.0": + version: 4.1.0 + resolution: "@graphql-codegen/plugin-helpers@npm:4.1.0" dependencies: "@graphql-tools/utils": ^9.0.0 change-case-all: 1.0.15 common-tags: 1.8.2 import-from: 4.0.0 lodash: ~4.17.0 - tslib: ~2.4.0 + tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 4d0c615738570681b5ffd3c07305a35d6aa3e5fd87c9199c0a670b95529ab865b1df978ce584d5b415107a567ac484e56a48db129a6d1d2eb8a254fbd3260e39 + checksum: a4d89909d7a62bd7a93041b6073b1d46e912b42f7af1d15c44f7ed595ab1185b6c257cccf8272e0ae36ba22724578449d0e21524e8dbc21fb375da0620687868 languageName: node linkType: hard -"@graphql-codegen/schema-ast@npm:^2.6.1": - version: 2.6.1 - resolution: "@graphql-codegen/schema-ast@npm:2.6.1" +"@graphql-codegen/schema-ast@npm:^3.0.1": + version: 3.0.1 + resolution: "@graphql-codegen/schema-ast@npm:3.0.1" dependencies: - "@graphql-codegen/plugin-helpers": ^3.1.2 + "@graphql-codegen/plugin-helpers": ^4.1.0 "@graphql-tools/utils": ^9.0.0 - tslib: ~2.4.0 + tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: f44338ac66e6a1f6238c33cdf65778bb467fe5a93767988135cb4e112d3be4d3c7e8aeeffe323754e8d6b0cbc5a52cb71452bfc42a15bc7031ebaa9b3d5da676 + checksum: 620aa67a4ae59ccb4609763b7347d05e2cec62bf1362be3e1e01fc00969cdbb858398542aa261128e5b5e3cb6808b77861bdcf82662e80326e72b418f25f465f languageName: node linkType: hard -"@graphql-codegen/typescript-resolvers@npm:^2.4.3": - version: 2.7.12 - resolution: "@graphql-codegen/typescript-resolvers@npm:2.7.12" +"@graphql-codegen/typescript-resolvers@npm:^3.0.0": + version: 3.1.0 + resolution: "@graphql-codegen/typescript-resolvers@npm:3.1.0" dependencies: - "@graphql-codegen/plugin-helpers": ^3.1.2 - "@graphql-codegen/typescript": ^2.8.7 - "@graphql-codegen/visitor-plugin-common": 2.13.7 + "@graphql-codegen/plugin-helpers": ^4.1.0 + "@graphql-codegen/typescript": ^3.0.1 + "@graphql-codegen/visitor-plugin-common": 3.0.1 "@graphql-tools/utils": ^9.0.0 auto-bind: ~4.0.0 - tslib: ~2.4.0 + tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: f8f60d972720f42f7407d91febfa4ae91f4e6cc2bd990babe85f8d1c77a3fa8dcc4ac85d8267ce24942703fc28ff6f38f4d8215e098f34fe1b35063aa08ef5a0 + checksum: 05888a9dfd1556fe15465a524750c36881334c231297e2e89bccb536450bed61fde2ba182ba4e79e34be846ce2e2439f3f6b31a67e7437f03a4e4ace62e84dcd languageName: node linkType: hard -"@graphql-codegen/typescript@npm:^2.4.2, @graphql-codegen/typescript@npm:^2.8.7": - version: 2.8.7 - resolution: "@graphql-codegen/typescript@npm:2.8.7" +"@graphql-codegen/typescript@npm:^3.0.0, @graphql-codegen/typescript@npm:^3.0.1": + version: 3.0.1 + resolution: "@graphql-codegen/typescript@npm:3.0.1" dependencies: - "@graphql-codegen/plugin-helpers": ^3.1.2 - "@graphql-codegen/schema-ast": ^2.6.1 - "@graphql-codegen/visitor-plugin-common": 2.13.7 + "@graphql-codegen/plugin-helpers": ^4.1.0 + "@graphql-codegen/schema-ast": ^3.0.1 + "@graphql-codegen/visitor-plugin-common": 3.0.1 auto-bind: ~4.0.0 - tslib: ~2.4.0 + tslib: ~2.5.0 peerDependencies: graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 67cbafc0dc8695222fd3b2db26c3fcd91148a1dba7bb5dcf71eaf6425dadbaa378c5703f2844e34718954aa283aa95919d8d4650bd1d0b85b22b404ffc3436fa + checksum: 56a369237971cc4a5a4987628ec0fcceed66ec03a292b3d17b6ffb64279185cd085be351268c55be6b152634c430e885b9c584d816aaa25f067d91c6996c329a languageName: node linkType: hard -"@graphql-codegen/visitor-plugin-common@npm:2.13.7": - version: 2.13.7 - resolution: "@graphql-codegen/visitor-plugin-common@npm:2.13.7" +"@graphql-codegen/visitor-plugin-common@npm:3.0.1": + version: 3.0.1 + resolution: "@graphql-codegen/visitor-plugin-common@npm:3.0.1" dependencies: - "@graphql-codegen/plugin-helpers": ^3.1.2 + "@graphql-codegen/plugin-helpers": ^4.1.0 "@graphql-tools/optimize": ^1.3.0 "@graphql-tools/relay-operation-optimizer": ^6.5.0 "@graphql-tools/utils": ^9.0.0 @@ -9427,10 +10235,10 @@ __metadata: dependency-graph: ^0.11.0 graphql-tag: ^2.11.0 parse-filepath: ^1.0.2 - tslib: ~2.4.0 + tslib: ~2.5.0 peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 4218af8b1542789d9c8c614ccb4362a2b4944d5c3a4a59909844fe9f1f8672825adb28deeb1b9a4d45410e1ae94cde9bd1f5c5d888ae53469260d6668814732e + checksum: 32bf553f73938581f634b1bbdf8616b1cb30cc85c73d7a8f6088d2a81a957e198c7c278edf6582c1eee7328a4463cea4725316b1743b87b26dc3e9fdd8318648 languageName: node linkType: hard @@ -9476,18 +10284,18 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/code-file-loader@npm:^7.3.13": - version: 7.3.13 - resolution: "@graphql-tools/code-file-loader@npm:7.3.13" +"@graphql-tools/code-file-loader@npm:^7.3.17": + version: 7.3.21 + resolution: "@graphql-tools/code-file-loader@npm:7.3.21" dependencies: - "@graphql-tools/graphql-tag-pluck": 7.4.0 - "@graphql-tools/utils": 9.1.1 + "@graphql-tools/graphql-tag-pluck": 7.5.0 + "@graphql-tools/utils": 9.2.1 globby: ^11.0.3 tslib: ^2.4.0 unixify: ^1.0.0 peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 18eeea4d3a89c15cf74d89630b647b6951487072f70a1b193d39c47b03533d3fe89f3e8f232b604db21b597768802b4884446b9820bf1febbdf1c094924be279 + checksum: 5d0d0a70bac673b4925a65f2283e3fcc5c093d2466a144cc577cb515d3de92ca2458d92842e02fe82a95d57d30ce6d2998b8ee5674393f7cd3df52f69671d565 languageName: node linkType: hard @@ -9651,6 +10459,22 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/graphql-tag-pluck@npm:7.5.0": + version: 7.5.0 + resolution: "@graphql-tools/graphql-tag-pluck@npm:7.5.0" + dependencies: + "@babel/parser": ^7.16.8 + "@babel/plugin-syntax-import-assertions": 7.20.0 + "@babel/traverse": ^7.16.8 + "@babel/types": ^7.16.8 + "@graphql-tools/utils": 9.2.1 + tslib: ^2.4.0 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 47edaacdeafc4a464f8a51e047cafbb82a9a2700536a5b075eb4afad43c80b513a7b0ac4ad49a1e8a74b566ed7dfd677ebb0d0a967fe1e2ee66408cd5d4f294b + languageName: node + linkType: hard + "@graphql-tools/import@npm:6.7.1": version: 6.7.1 resolution: "@graphql-tools/import@npm:6.7.1" @@ -9678,17 +10502,17 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/load@npm:7.8.0, @graphql-tools/load@npm:^7.5.5": - version: 7.8.0 - resolution: "@graphql-tools/load@npm:7.8.0" +"@graphql-tools/load@npm:^7.5.5, @graphql-tools/load@npm:^7.8.0": + version: 7.8.10 + resolution: "@graphql-tools/load@npm:7.8.10" dependencies: - "@graphql-tools/schema": 9.0.4 - "@graphql-tools/utils": 8.12.0 + "@graphql-tools/schema": 9.0.14 + "@graphql-tools/utils": 9.1.4 p-limit: 3.1.0 tslib: ^2.4.0 peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 24ade442e13429d087ceac905cef4e6a14d0961e2b231adf91180a4d674de08ac6de66e6103c3e118b6ccf24065492bb69f5c99cb336e71a579ab382c8574791 + checksum: 9b1fd7041fc69072c4f7d6bc320c9de7f990e1792db567d3d913b9cc77f5c164ffee968dd90a688d89856e1e8c01beb8e1bbe9f22464fa8c563553229c3a3f28 languageName: node linkType: hard @@ -9728,27 +10552,27 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/merge@npm:8.3.15, @graphql-tools/merge@npm:^8.2.6": - version: 8.3.15 - resolution: "@graphql-tools/merge@npm:8.3.15" +"@graphql-tools/merge@npm:8.3.16": + version: 8.3.16 + resolution: "@graphql-tools/merge@npm:8.3.16" dependencies: "@graphql-tools/utils": 9.1.4 tslib: ^2.4.0 peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 7823d3b3a2bb7a661826eb8fce8a4fcb17439224818aa08ca814d7b25324b66b790cf8e7fa083dfa9f41b2213355fb1f0db88d1e7334788347a6d69c8f5555e9 + checksum: 4d7fa594e0d97b6d596c85619a8203cb9c88452b75efac4191697de00db5d043639ed47f6bf0a3ccc11e8638714d92a4a8bc37e77b61b46790ddf4a1e5d4bb87 languageName: node linkType: hard -"@graphql-tools/merge@npm:8.3.6": - version: 8.3.6 - resolution: "@graphql-tools/merge@npm:8.3.6" +"@graphql-tools/merge@npm:8.3.18, @graphql-tools/merge@npm:^8.2.6": + version: 8.3.18 + resolution: "@graphql-tools/merge@npm:8.3.18" dependencies: - "@graphql-tools/utils": 8.12.0 + "@graphql-tools/utils": 9.2.1 tslib: ^2.4.0 peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 3e45ebff0dce9524e72c4af1d2b9799c16515c1236290e07cd68fb2226c4e54734e2444d89a64b387b7ba991d15c6f948fdfc20c77a74b1d24babddd865ff32a + checksum: 0834fcf13dec5ee92e39f2e1b27a098654e2db20bf6ef6f43d7efce56b7d750caa59dd16aacb211b8f5a198d29c19d36471401762f5a34e0bc22657285dcae92 languageName: node linkType: hard @@ -9861,31 +10685,31 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/schema@npm:9.0.4": - version: 9.0.4 - resolution: "@graphql-tools/schema@npm:9.0.4" +"@graphql-tools/schema@npm:9.0.14": + version: 9.0.14 + resolution: "@graphql-tools/schema@npm:9.0.14" dependencies: - "@graphql-tools/merge": 8.3.6 - "@graphql-tools/utils": 8.12.0 - tslib: ^2.4.0 - value-or-promise: 1.0.11 - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 0644ba225ff7fb03c6fb7f026b6a77a4ac5dd14fd10bb562ea0072bfe0258620fd4789b851b0b97007db8754d0b7d88a1061bb98bacc679b22e2eb7706e79e0e - languageName: node - linkType: hard - -"@graphql-tools/schema@npm:^9.0.0": - version: 9.0.13 - resolution: "@graphql-tools/schema@npm:9.0.13" - dependencies: - "@graphql-tools/merge": 8.3.15 + "@graphql-tools/merge": 8.3.16 "@graphql-tools/utils": 9.1.4 tslib: ^2.4.0 value-or-promise: 1.0.12 peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: fa5c5033c79e3ab210207c68bf80729448f0ae0bf071e029a218043d952dec8352b0d4ad467ca789a08e292d60c1ee111d266a2193bd50759c9eb82e8f9ad586 + checksum: ae958892363fe8b1715a9fd3c4bdb56d0d5d429693dbb9774ce930d104ed5a7c94db3c41c12d91fd930bf90ab186337d3ba660e765a99a98bde892f0d2dcf40e + languageName: node + linkType: hard + +"@graphql-tools/schema@npm:^9.0.0": + version: 9.0.16 + resolution: "@graphql-tools/schema@npm:9.0.16" + dependencies: + "@graphql-tools/merge": 8.3.18 + "@graphql-tools/utils": 9.2.1 + tslib: ^2.4.0 + value-or-promise: 1.0.12 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: ecbd27b4a36424c8d4b2206b1f2d2ec2cff3c66f52ec452b17977642017ca7f00df4df44f2d0426174ceea9596474d0656bc0227f4c1149f2288abb6d186cd68 languageName: node linkType: hard @@ -9912,17 +10736,6 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/utils@npm:8.12.0": - version: 8.12.0 - resolution: "@graphql-tools/utils@npm:8.12.0" - dependencies: - tslib: ^2.4.0 - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 24edc6ba3bcfa9a4c1d1d37117c3f96d847beed9638325083c32c6ec9674729dc89fc8cc389d317ae5d9dba22e91443bd9788f1dc8de91a1b6f1e592112bd48f - languageName: node - linkType: hard - "@graphql-tools/utils@npm:8.6.9": version: 8.6.9 resolution: "@graphql-tools/utils@npm:8.6.9" @@ -9978,7 +10791,7 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/utils@npm:9.1.4, @graphql-tools/utils@npm:^9.0.0, @graphql-tools/utils@npm:^9.1.1": +"@graphql-tools/utils@npm:9.1.4": version: 9.1.4 resolution: "@graphql-tools/utils@npm:9.1.4" dependencies: @@ -9989,6 +10802,18 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/utils@npm:9.2.1, @graphql-tools/utils@npm:^9.0.0, @graphql-tools/utils@npm:^9.1.1": + version: 9.2.1 + resolution: "@graphql-tools/utils@npm:9.2.1" + dependencies: + "@graphql-typed-document-node/core": ^3.1.1 + tslib: ^2.4.0 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 94ed12df5f49e5c338322ffd931236a687a3d5c443bf499f9baab5d4fcd9792234111142be8aa506a01ca2e82732996c4e1d8f6159ff9cc7fdc5c97f63e55226 + languageName: node + linkType: hard + "@graphql-tools/wrap@npm:9.2.23": version: 9.2.23 resolution: "@graphql-tools/wrap@npm:9.2.23" @@ -10028,13 +10853,13 @@ __metadata: languageName: node linkType: hard -"@grpc/grpc-js@npm:~1.7.0": - version: 1.7.0 - resolution: "@grpc/grpc-js@npm:1.7.0" +"@grpc/grpc-js@npm:~1.8.0": + version: 1.8.9 + resolution: "@grpc/grpc-js@npm:1.8.9" dependencies: "@grpc/proto-loader": ^0.7.0 "@types/node": ">=12.12.47" - checksum: 22eee49f9f5aea1abf9566426d993fe4457c94c2afacd3dfb932cc46dbc06ad6ce8f1422fac357d592dd0c75a5cf03405ea15310064ab3c45e1e418f22c48c2b + checksum: 40408af0c0e2b61dbb0fe1c246ae30404c89c601f3e9f9f8447f82d01d8dc0c8befccf2afe9ded07dfc8bd89c927bcbfb5925d4b1ae887299112b2ab549f519c languageName: node linkType: hard @@ -10133,6 +10958,7 @@ __metadata: resolution: "@internal/plugin-todo-list-backend@workspace:plugins/example-todo-list-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" @@ -10142,7 +10968,7 @@ __metadata: "@types/uuid": ^8.0.0 express: ^4.17.1 express-promise-router: ^4.1.0 - msw: ^0.49.0 + msw: ^1.0.0 supertest: ^6.1.6 uuid: ^8.3.2 winston: ^3.2.1 @@ -10161,7 +10987,7 @@ __metadata: "@backstage/test-utils": "workspace:^" "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.49.0 + msw: ^1.0.0 languageName: unknown linkType: soft @@ -10183,7 +11009,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 - msw: ^0.49.0 + msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -10516,460 +11342,6 @@ __metadata: languageName: node linkType: hard -"@jimp/bmp@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/bmp@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - bmp-js: ^0.1.0 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 3a5a7600add0edf68a0d8f8397dd0ca37fa20d93b4d9ea8321875acd92e4a632a31ac1af0fc2129cff76cdc77aa77b359327613f822c0bc48781f007459e44f8 - languageName: node - linkType: hard - -"@jimp/core@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/core@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - any-base: ^1.1.0 - buffer: ^5.2.0 - core-js: ^3.4.1 - exif-parser: ^0.1.12 - file-type: ^9.0.0 - load-bmfont: ^1.3.1 - mkdirp: ^0.5.1 - phin: ^2.9.1 - pixelmatch: ^4.0.2 - tinycolor2: ^1.4.1 - checksum: f67db147a114b7a220621b80f9c65cf0d1434f4bd7ab8203b44246b79093f9ec46170a6205755eae7fe53c14d2935d8565f5a2185ccd4495ae1cd6f13bcf4852 - languageName: node - linkType: hard - -"@jimp/custom@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/custom@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/core": ^0.10.3 - core-js: ^3.4.1 - checksum: 8a282c9387cb975a533d140a8e0c9f7c23b12b6e6a92e9a1e979b7cc7d89e623189c47b0d0119aa7e497149f0d9a85e4ec14a605921527340c307b7750ba519a - languageName: node - linkType: hard - -"@jimp/gif@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/gif@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - omggif: ^1.0.9 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: a23253e6762e6f19b0fa6deaa9db3bca25b313eb5ea506dd3d844407598d86cf4188cc92c091019a4257450821ffe24f0475cc2e4a1ef24998d95d176078cf2f - languageName: node - linkType: hard - -"@jimp/jpeg@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/jpeg@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - jpeg-js: ^0.3.4 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: cb830ddf3fddbc4f833086ffbf8f21e0c73863522cce86d6f6c82d4f0183d0619ee280736c5ed07563bfcf9f884fd96bdb845a95c5a8a6e11f961f4e95cb4b24 - languageName: node - linkType: hard - -"@jimp/plugin-blit@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-blit@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 5858557359eb0405c29741c8ffe98b4bb26b42564c178402e21b74733c02e04233d2a1af095cff4cd2864ab43d79e01033559f250b3eb879600138007a7d7453 - languageName: node - linkType: hard - -"@jimp/plugin-blur@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-blur@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: d3a2e14d4dc5f3b5ad010888e70193cc8f7327aeda67989eba3f49de2e5324fa6adf89e3a7f1597343f59688ce6e299ad5818aa6c670ed8596ddfba0ca281db3 - languageName: node - linkType: hard - -"@jimp/plugin-circle@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-circle@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: d351c85f4a9aeb404302a03f5d99e6486b95f271f2c628758c8e3d1574bc268278ffc34d9d00d3221d6079431b06a5e6a5a3922863994000b5eb3c81f8c27d35 - languageName: node - linkType: hard - -"@jimp/plugin-color@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-color@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - tinycolor2: ^1.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 2014802f3b0465abbd7682336836183293a26315acbb84826f5207a374f807c574e120790738dcf51aba2cb7e9a481003289ef8bf429facb9fe36a19ce5b6924 - languageName: node - linkType: hard - -"@jimp/plugin-contain@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-contain@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-blit": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - "@jimp/plugin-scale": ">=0.3.5" - checksum: 91d77621a350a733839fa8bc15880bc74ac666682a651e676797207dc4f55cba465f3695f6280f3d05122d67067a0a94f3905f323410d880484dd769055c9b49 - languageName: node - linkType: hard - -"@jimp/plugin-cover@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-cover@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-crop": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - "@jimp/plugin-scale": ">=0.3.5" - checksum: ca664509c2d42e987a56e80fa848ec3e5590a1f919c0a607d7a03b8192571456c0287a2f768785bf816ecfe7889a7a786490a408866acc94ce5306b39ca82f6b - languageName: node - linkType: hard - -"@jimp/plugin-crop@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-crop@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 54e79b6fa831f813f28dac53187dfa1684d00ece8a98433c30c1e61333ba2b5373758889d202418a55c99dffe235daf153454f35691324b42dd9a103a65d3bd2 - languageName: node - linkType: hard - -"@jimp/plugin-displace@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-displace@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 17b5792ce07ac3b7b5aa8c457626fb46ded0a8f05027c4580fac1b22428e7c2c0ff04e0ad4f7249c179322afa7921e57afc88ace03e2d08b67e864c24ef5c060 - languageName: node - linkType: hard - -"@jimp/plugin-dither@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-dither@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 77caaceebccceb73e32616fd79517d9640d217403577715e08146bf2a67599822070be8a51783ad76402e0303f6bc9afcebf75f75cc73ae44c684e3c1e1248ea - languageName: node - linkType: hard - -"@jimp/plugin-fisheye@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-fisheye@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: f60d9c6c40d6fa1b85658ffa23568f95aeab1635165f2b74266b128be0f9b4bf963cfa755e5a43b7f18421340a4bb7b628ab75821b34b602924154b55c0f39ce - languageName: node - linkType: hard - -"@jimp/plugin-flip@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-flip@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-rotate": ">=0.3.5" - checksum: 6542b9e595e3b95cf8ac741226e9089ab1edd3fa19bc4ac159afd5bc3801a55f6ee93ade029626dbde7977eb1fbca51c71d9c6546b5285754eb29b54a7f5f5ff - languageName: node - linkType: hard - -"@jimp/plugin-gaussian@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-gaussian@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 465ca178b9a9062eb6db4a57fe3b47236270bad9600cfb4aae162aad43f20e5cf90c19bf77bd08867f25abd0679c6018412e0ad8cb3777640dad5c3f51e64dd9 - languageName: node - linkType: hard - -"@jimp/plugin-invert@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-invert@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 917419301a961541466e04c7fc974fc95f35e3f95f75e6bfef141270f06b68bcdf61321cad2d545cd8115a6e3a99992345f964bdb94cbdb48aaf42885b8fcae7 - languageName: node - linkType: hard - -"@jimp/plugin-mask@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-mask@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 7870fe46d9fcec6145476bcceed23568012e87953772e7ea2b82c8ed48375ee525db6b977816574b47e08c595bd6123c8d9cd24a95e649677d0ec1fada4e6831 - languageName: node - linkType: hard - -"@jimp/plugin-normalize@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-normalize@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: f34ebf3ef03d83b2cb9a8779d39a3e10707aeae4dfe67e1bb30202589e72d09c0e9a61b429de4e4886728f0faabbb83a6ff081f2e9bf79b517e2ea387c53a9a4 - languageName: node - linkType: hard - -"@jimp/plugin-print@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-print@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - load-bmfont: ^1.4.0 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-blit": ">=0.3.5" - checksum: 772a7d82741af583293006f699bbbaf116c264327412ea6ed0f3b7fb14d0dcbef51278e6fce3e966ad23e655114bfe949106d551d06459819b26e461638e8a5e - languageName: node - linkType: hard - -"@jimp/plugin-resize@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-resize@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: aff691d2936bd88d59a68b36a6f06b306d0f7c654baec7b43371e6b250397b3a19d0b98a620ad26738ed51357f5899a8002761d8f6f0ae6074e6c63905895a5d - languageName: node - linkType: hard - -"@jimp/plugin-rotate@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-rotate@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-blit": ">=0.3.5" - "@jimp/plugin-crop": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - checksum: f1771365753a64387e455006467dd41e09c1e329180e57df77ef78f72dfac7d889e5d03adae1038f4c6ac081a9d5113c59541983fdda6bf08da9af852650c468 - languageName: node - linkType: hard - -"@jimp/plugin-scale@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-scale@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - checksum: 356e8ab040bf842845a29cc0a6d7170fd15c1a9e9407028b88771bff01ecb6870c8f26d15b59b432914c430e611f36f54f932aaa7a68dcc9e32efae2f6dd1946 - languageName: node - linkType: hard - -"@jimp/plugin-shadow@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-shadow@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-blur": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - checksum: 7f07387160b31732b3d43a2c4d44f8716be7a08b2f0fcfd58d18ca76ed62d825629c6273434bd23d02ade6a18bc8d7c7020eb9ab0ccc95acd85e718b5ab2c7a0 - languageName: node - linkType: hard - -"@jimp/plugin-threshold@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-threshold@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-color": ">=0.8.0" - "@jimp/plugin-resize": ">=0.8.0" - checksum: 550de37f82ccfaacb954b0e183a7bfa77f80f33ec9f5b08756ce778675284e486a2b5860c69c6d15a38d1d7030f1896e1ac79e8675603ead9e36c075f3cf636e - languageName: node - linkType: hard - -"@jimp/plugins@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugins@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/plugin-blit": ^0.10.3 - "@jimp/plugin-blur": ^0.10.3 - "@jimp/plugin-circle": ^0.10.3 - "@jimp/plugin-color": ^0.10.3 - "@jimp/plugin-contain": ^0.10.3 - "@jimp/plugin-cover": ^0.10.3 - "@jimp/plugin-crop": ^0.10.3 - "@jimp/plugin-displace": ^0.10.3 - "@jimp/plugin-dither": ^0.10.3 - "@jimp/plugin-fisheye": ^0.10.3 - "@jimp/plugin-flip": ^0.10.3 - "@jimp/plugin-gaussian": ^0.10.3 - "@jimp/plugin-invert": ^0.10.3 - "@jimp/plugin-mask": ^0.10.3 - "@jimp/plugin-normalize": ^0.10.3 - "@jimp/plugin-print": ^0.10.3 - "@jimp/plugin-resize": ^0.10.3 - "@jimp/plugin-rotate": ^0.10.3 - "@jimp/plugin-scale": ^0.10.3 - "@jimp/plugin-shadow": ^0.10.3 - "@jimp/plugin-threshold": ^0.10.3 - core-js: ^3.4.1 - timm: ^1.6.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 28833f4ce43f911c41b5c251759f5e38406ba99249a7c00d11a81e4ce58027eb993ca0c220601c3702832c2855dbb0554762bf724107bb4fdd0a2c1cf8753895 - languageName: node - linkType: hard - -"@jimp/png@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/png@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - pngjs: ^3.3.3 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 1520b669d0dc6a0b47d7357c5e75a080e3a56db0459fad5c56e299b03c4a7bf560540480b08fa2e1eaf283ba153efb26a3e90d90fe373069c6c6184f7ea3e163 - languageName: node - linkType: hard - -"@jimp/tiff@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/tiff@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - core-js: ^3.4.1 - utif: ^2.0.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 6ff41bc962afdbcd223b331beed232b5aecf2b0ba06cccddaef5b5d4b925fa51d6a26d226080baabd25d7dceb0b9b0e5c69b60c44494822cb69988c3221c6514 - languageName: node - linkType: hard - -"@jimp/types@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/types@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/bmp": ^0.10.3 - "@jimp/gif": ^0.10.3 - "@jimp/jpeg": ^0.10.3 - "@jimp/png": ^0.10.3 - "@jimp/tiff": ^0.10.3 - core-js: ^3.4.1 - timm: ^1.6.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 68ea0f1a1f3116ba779c898c9fb38fb7b822addaa14ac153aea4cdc9e274efc34a46a6ad4c8a9978fb88b4ab5a826190e28b89b431dce735ee760997f2d66678 - languageName: node - linkType: hard - -"@jimp/utils@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/utils@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - core-js: ^3.4.1 - regenerator-runtime: ^0.13.3 - checksum: bd36b8bbaaf878190afe357f1bcebed9215dae503f06603039e6a106045588003b845150cb5a5ba6dd4d973e7e7b95779158ae921011fb6bd79083c8cb177804 - languageName: node - linkType: hard - "@josephg/resolvable@npm:^1.0.0": version: 1.0.1 resolution: "@josephg/resolvable@npm:1.0.1" @@ -11046,6 +11418,15 @@ __metadata: languageName: node linkType: hard +"@jsdoc/salty@npm:^0.2.1": + version: 0.2.3 + resolution: "@jsdoc/salty@npm:0.2.3" + dependencies: + lodash: ^4.17.21 + checksum: d2b29089e6ba942bf3228087d35379ada248c34f136357d1a4bf5c0350a0d5bde0f90dc68d5e7271728bac1951f85fe749075df0a32a9d584ad5d198492bf7f1 + languageName: node + linkType: hard + "@juggle/resize-observer@npm:^3.3.1": version: 3.3.1 resolution: "@juggle/resize-observer@npm:3.3.1" @@ -11054,51 +11435,49 @@ __metadata: linkType: hard "@keyv/memcache@npm:^1.3.5": - version: 1.3.5 - resolution: "@keyv/memcache@npm:1.3.5" + version: 1.3.6 + resolution: "@keyv/memcache@npm:1.3.6" dependencies: json-buffer: ^3.0.1 memjs: ^1.3.0 - checksum: c457863aeff1c5886c273eafa5fd79a4287cae1d9d3721b6ce5aa699033428ec8930db044224a6ab63d951cdac2408850737d0adc65f0a502bf04cd7dbe0d15e + checksum: 3360055b6e22d1be7a77341dc99bafb5434d3288dace94067f3307f6d9498f984dcb0d8452a5b2d97ddca8f8c973caf7cc1332e0ffada1c6dbfb0b2a2d42451a languageName: node linkType: hard "@keyv/redis@npm:^2.5.3": - version: 2.5.4 - resolution: "@keyv/redis@npm:2.5.4" + version: 2.5.5 + resolution: "@keyv/redis@npm:2.5.5" dependencies: - ioredis: ^5.2.4 - checksum: 02f557e0bc0343aea44b8dd4b6ff273da5833b935236c4036fa89e90b1d92d9688dafa064d5318d26744a6c72497cd4c41dcac825cb8d5919271e3a25a3cadbb + ioredis: ^5.3.0 + checksum: dc4c4e9c0a44dec1166a6a4481a42342289b4618136c2f640306bee381cf30d737346b064ecc369fc8ebdd2baaaba47e099aabe9f37839911171a9c006d75e62 languageName: node linkType: hard -"@kubernetes/client-node@npm:0.18.0": - version: 0.18.0 - resolution: "@kubernetes/client-node@npm:0.18.0" +"@kubernetes/client-node@npm:0.18.1": + version: 0.18.1 + resolution: "@kubernetes/client-node@npm:0.18.1" dependencies: "@types/js-yaml": ^4.0.1 - "@types/node": ^10.12.0 + "@types/node": ^18.11.17 "@types/request": ^2.47.1 - "@types/ws": ^6.0.1 + "@types/ws": ^8.5.3 byline: ^5.0.0 - execa: 5.0.0 - isomorphic-ws: ^4.0.1 + isomorphic-ws: ^5.0.0 js-yaml: ^4.1.0 - jsonpath-plus: ^0.19.0 + jsonpath-plus: ^7.2.0 openid-client: ^5.3.0 request: ^2.88.0 rfc4648: ^1.3.0 - shelljs: ^0.8.5 stream-buffers: ^3.0.2 tar: ^6.1.11 tmp-promise: ^3.0.2 tslib: ^2.4.1 underscore: ^1.13.6 - ws: ^7.3.1 + ws: ^8.11.0 dependenciesMeta: openid-client: optional: true - checksum: ea973ddf793b7351da5e1beaac461b8f2ae581cb6d65b62669f248f8cb345f0d54d4b888820fb49f471bc60bace37e706120f46d10d3726a63ccaed8b6cb401b + checksum: 7205f10256f182a0292d483195fab7f4d9b500b3a47e6861dcf2fa8851c0bfd554e4f9a9f832ff76500efefb0422df2ecf2e378c5c3a4290e86abd8bc69ef11d languageName: node linkType: hard @@ -11203,7 +11582,45 @@ __metadata: languageName: node linkType: hard -"@material-ui/core@npm:^4.11.0, @material-ui/core@npm:^4.11.3, @material-ui/core@npm:^4.12.1, @material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4, @material-ui/core@npm:^4.9.10, @material-ui/core@npm:^4.9.13": +"@material-ui/core@npm:^3.9.3": + version: 3.9.4 + resolution: "@material-ui/core@npm:3.9.4" + dependencies: + "@babel/runtime": ^7.2.0 + "@material-ui/system": ^3.0.0-alpha.0 + "@material-ui/utils": ^3.0.0-alpha.2 + "@types/jss": ^9.5.6 + "@types/react-transition-group": ^2.0.8 + brcast: ^3.0.1 + classnames: ^2.2.5 + csstype: ^2.5.2 + debounce: ^1.1.0 + deepmerge: ^3.0.0 + dom-helpers: ^3.2.1 + hoist-non-react-statics: ^3.2.1 + is-plain-object: ^2.0.4 + jss: ^9.8.7 + jss-camel-case: ^6.0.0 + jss-default-unit: ^8.0.2 + jss-global: ^3.0.0 + jss-nested: ^6.0.1 + jss-props-sort: ^6.0.0 + jss-vendor-prefixer: ^7.0.0 + normalize-scroll-left: ^0.1.2 + popper.js: ^1.14.1 + prop-types: ^15.6.0 + react-event-listener: ^0.6.2 + react-transition-group: ^2.2.1 + recompose: 0.28.0 - 0.30.0 + warning: ^4.0.1 + peerDependencies: + react: ^16.3.0 + react-dom: ^16.3.0 + checksum: c3923bfba4ef4449fbdcd37712ec0d11393ffa396c575a05b5f9530ca1e714028882d39402eb6918dad49f2eb1c53b261cfd0f2dfed494c2dfd96ee335336b44 + languageName: node + linkType: hard + +"@material-ui/core@npm:^4.11.0, @material-ui/core@npm:^4.11.3, @material-ui/core@npm:^4.12.1, @material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4, @material-ui/core@npm:^4.9.13": version: 4.12.4 resolution: "@material-ui/core@npm:4.12.4" dependencies: @@ -11361,6 +11778,21 @@ __metadata: languageName: node linkType: hard +"@material-ui/system@npm:^3.0.0-alpha.0": + version: 3.0.0-alpha.2 + resolution: "@material-ui/system@npm:3.0.0-alpha.2" + dependencies: + "@babel/runtime": ^7.2.0 + deepmerge: ^3.0.0 + prop-types: ^15.6.0 + warning: ^4.0.1 + peerDependencies: + react: ^16.3.0 + react-dom: ^16.3.0 + checksum: 3aacb8b4afe2a820844b90905cfcd86ab934108cd719f11713ee46f8a4da30cb8c50e86219e1440b3c6dd333a09d4123cd1990d9c51862c1d56ec2b36e4d7ded + languageName: node + linkType: hard + "@material-ui/system@npm:^4.12.2": version: 4.12.2 resolution: "@material-ui/system@npm:4.12.2" @@ -11404,6 +11836,20 @@ __metadata: languageName: node linkType: hard +"@material-ui/utils@npm:^3.0.0-alpha.2": + version: 3.0.0-alpha.3 + resolution: "@material-ui/utils@npm:3.0.0-alpha.3" + dependencies: + "@babel/runtime": ^7.2.0 + prop-types: ^15.6.0 + react-is: ^16.6.3 + peerDependencies: + react: ^16.3.0 + react-dom: ^16.3.0 + checksum: 589a16245338c374b604a793d9f7a05bd932c9710f78261e46176b74a61d5d308b3e36dab688988ee5177d689c53d9e9da4649eca009ea11d8aac948db27af50 + languageName: node + linkType: hard + "@material-ui/utils@npm:^4.11.2, @material-ui/utils@npm:^4.11.3, @material-ui/utils@npm:^4.7.1": version: 4.11.3 resolution: "@material-ui/utils@npm:4.11.3" @@ -11477,7 +11923,7 @@ __metadata: languageName: node linkType: hard -"@microsoft/microsoft-graph-types@npm:^2.6.0": +"@microsoft/microsoft-graph-types@npm:^2.25.0, @microsoft/microsoft-graph-types@npm:^2.6.0": version: 2.25.0 resolution: "@microsoft/microsoft-graph-types@npm:2.25.0" checksum: b311385e0a51f827082af5a016f35b1d6cd4d861121e8767b3279ab1a17475977cddbbdcc208abdddf0f953fd6ce4634d3c302e317cf50af4cb6111385347b4f @@ -11503,7 +11949,7 @@ __metadata: languageName: node linkType: hard -"@mswjs/cookies@npm:^0.2.0, @mswjs/cookies@npm:^0.2.2": +"@mswjs/cookies@npm:^0.2.2": version: 0.2.2 resolution: "@mswjs/cookies@npm:0.2.2" dependencies: @@ -11513,20 +11959,6 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.15.1": - version: 0.15.3 - resolution: "@mswjs/interceptors@npm:0.15.3" - dependencies: - "@open-draft/until": ^1.0.3 - "@xmldom/xmldom": ^0.7.5 - debug: ^4.3.3 - headers-polyfill: ^3.0.4 - outvariant: ^1.2.1 - strict-event-emitter: ^0.2.0 - checksum: 4c8e1d16f4a7b6133c02f1e13e9fc20af82dfa90106209036a9b3d85bb6ef4cce3b41312abde4bc9ccf59aa0b448e506d670bc6877b2ba25aad4f4b348d1454a - languageName: node - linkType: hard - "@mswjs/interceptors@npm:^0.17.5": version: 0.17.5 resolution: "@mswjs/interceptors@npm:0.17.5" @@ -11838,20 +12270,20 @@ __metadata: linkType: hard "@octokit/auth-app@npm:^4.0.0": - version: 4.0.8 - resolution: "@octokit/auth-app@npm:4.0.8" + version: 4.0.9 + resolution: "@octokit/auth-app@npm:4.0.9" dependencies: "@octokit/auth-oauth-app": ^5.0.0 "@octokit/auth-oauth-user": ^2.0.0 "@octokit/request": ^6.0.0 "@octokit/request-error": ^3.0.0 - "@octokit/types": ^8.0.0 + "@octokit/types": ^9.0.0 "@types/lru-cache": ^5.1.0 deprecation: ^2.3.1 lru-cache: ^6.0.0 universal-github-app-jwt: ^1.1.1 universal-user-agent: ^6.0.0 - checksum: 819c048db51ee041697bb4cbba5fc39fbc0849e76cda63dd34ff9e77d60be62c5f93c478a9c1edf6643a88afaefc7aefe562e86b73f6ef6874df1bc797b9b617 + checksum: 3846ebeda40bf88684fa310741ec4b5838d57e18ba5c38986e422161e1ce95c750053fbb2aa18bdce120d6eb857d16bb8e9988597c3dfda40fec985dbdda1b2a languageName: node linkType: hard @@ -11999,13 +12431,13 @@ __metadata: linkType: hard "@octokit/graphql@npm:^5.0.0": - version: 5.0.4 - resolution: "@octokit/graphql@npm:5.0.4" + version: 5.0.5 + resolution: "@octokit/graphql@npm:5.0.5" dependencies: "@octokit/request": ^6.0.0 - "@octokit/types": ^8.0.0 + "@octokit/types": ^9.0.0 universal-user-agent: ^6.0.0 - checksum: 8cf65cf7e6608cf3cbc96a2fa902172b4d5dc30e88ee0bae3711bf467a25b828b10cce1aaabb7f82a7580bfbcf7028b91d1dd1a894940945e38ca2deb6509754 + checksum: eb2d1a6305a3d1f55ff0ce92fb88b677f0bb789757152d58a79ef61171fb65ecf6fe18d6c27e236c0cee6a0c2600c2cb8370f5ac7184f8e9361c085aa4555bb1 languageName: node linkType: hard @@ -12091,6 +12523,13 @@ __metadata: languageName: node linkType: hard +"@octokit/openapi-types@npm:^16.0.0": + version: 16.0.0 + resolution: "@octokit/openapi-types@npm:16.0.0" + checksum: 844f30a545da380d63c712e0eb733366bc567d1aab34529c79fdfbec3d73810e81d83f06fdab13058a5cbc7dae786db1a9b90b5b61b1e606854ee45d5ec5f194 + languageName: node + linkType: hard + "@octokit/plugin-paginate-rest@npm:^2.6.2": version: 2.7.0 resolution: "@octokit/plugin-paginate-rest@npm:2.7.0" @@ -12124,14 +12563,14 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-paginate-rest@npm:^5.0.0": - version: 5.0.1 - resolution: "@octokit/plugin-paginate-rest@npm:5.0.1" +"@octokit/plugin-paginate-rest@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/plugin-paginate-rest@npm:6.0.0" dependencies: - "@octokit/types": ^8.0.0 + "@octokit/types": ^9.0.0 peerDependencies: "@octokit/core": ">=4" - checksum: cfded297d9f66c7607bd34075eb0c5f7278a1617d6be86115997c0757151c9be2fcf7a8849c2a5cebab56931a263b5cc35742b6227935afe77f5ed61b0627a3d + checksum: 4ad89568d883373898b733837cada7d849d51eef32157c11d4a81cef5ce8e509720d79b46918cada3c132f9b29847e383f17b7cd5c39ede7c93cdcd2f850b47f languageName: node linkType: hard @@ -12156,7 +12595,7 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-rest-endpoint-methods@npm:^6.0.0, @octokit/plugin-rest-endpoint-methods@npm:^6.7.0": +"@octokit/plugin-rest-endpoint-methods@npm:^6.0.0": version: 6.7.0 resolution: "@octokit/plugin-rest-endpoint-methods@npm:6.7.0" dependencies: @@ -12168,6 +12607,18 @@ __metadata: languageName: node linkType: hard +"@octokit/plugin-rest-endpoint-methods@npm:^7.0.0": + version: 7.0.1 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:7.0.1" + dependencies: + "@octokit/types": ^9.0.0 + deprecation: ^2.3.1 + peerDependencies: + "@octokit/core": ">=3" + checksum: cdb8734ec960f75acc2405284920c58efac9a71b1c3b2a71662b9100ffbc22dac597150acff017a93459c57e9a492d9e1c27872b068387dbb90597de75065fcf + languageName: node + linkType: hard + "@octokit/plugin-retry@npm:^3.0.9": version: 3.0.9 resolution: "@octokit/plugin-retry@npm:3.0.9" @@ -12227,16 +12678,16 @@ __metadata: linkType: hard "@octokit/request@npm:^6.0.0": - version: 6.2.2 - resolution: "@octokit/request@npm:6.2.2" + version: 6.2.3 + resolution: "@octokit/request@npm:6.2.3" dependencies: "@octokit/endpoint": ^7.0.0 "@octokit/request-error": ^3.0.0 - "@octokit/types": ^8.0.0 + "@octokit/types": ^9.0.0 is-plain-object: ^5.0.0 node-fetch: ^2.6.7 universal-user-agent: ^6.0.0 - checksum: adbeb38807c60b53d32d9b69be0c1f861c26698bc6f5f3f7e05d26972290dc4867827dd333bdd801818c347e5723efd049a2b9848c6c8bf74a2032968dede0ff + checksum: fef4097be8375d20bb0b3276d8a3adf866ec628f2b0664d334f3c29b92157da847899497abdc7a5be540053819b55564990543175ad48f04e9e6f25f0395d4d3 languageName: node linkType: hard @@ -12253,14 +12704,14 @@ __metadata: linkType: hard "@octokit/rest@npm:^19.0.3": - version: 19.0.5 - resolution: "@octokit/rest@npm:19.0.5" + version: 19.0.7 + resolution: "@octokit/rest@npm:19.0.7" dependencies: "@octokit/core": ^4.1.0 - "@octokit/plugin-paginate-rest": ^5.0.0 + "@octokit/plugin-paginate-rest": ^6.0.0 "@octokit/plugin-request-log": ^1.0.4 - "@octokit/plugin-rest-endpoint-methods": ^6.7.0 - checksum: ed4c36859aafb64e23f7f708086fe7e2ecda17ffd8c1594817d539f766f5855af79f17f5d225d96d34cd9b97cbfea988dac3df39a7cc928b2d2b7b75ed981056 + "@octokit/plugin-rest-endpoint-methods": ^7.0.0 + checksum: 1f970c4de2cf3d1691d3cf5dd4bfa5ac205629e76417b5c51561e1beb5b4a7e6c65ba647f368727e67e5243418e06ca9cdafd9e733173e1529385d4f4d053d3d languageName: node linkType: hard @@ -12300,6 +12751,15 @@ __metadata: languageName: node linkType: hard +"@octokit/types@npm:^9.0.0": + version: 9.0.0 + resolution: "@octokit/types@npm:9.0.0" + dependencies: + "@octokit/openapi-types": ^16.0.0 + checksum: 5c7f5cca8f00f7c4daa0d00f4fe991c1598ec47cd6ced50b1c5fbe9721bb9dee0adc2acdee265a3a715bb984e53ef3dc7f1cfb7326f712c6d809d59fc5c6648d + languageName: node + linkType: hard + "@octokit/webhooks-methods@npm:^3.0.0": version: 3.0.2 resolution: "@octokit/webhooks-methods@npm:3.0.2" @@ -12307,22 +12767,22 @@ __metadata: languageName: node linkType: hard -"@octokit/webhooks-types@npm:6.7.0": - version: 6.7.0 - resolution: "@octokit/webhooks-types@npm:6.7.0" - checksum: 407a362f88c2f46261e973ba8e2a624a971134cb3e30a3cddcb08c192f1af4f02d49f6a624819b6ee72e6abe5a214923ffb2b80ed40b0c1717f71fb91783e3d3 +"@octokit/webhooks-types@npm:6.10.0": + version: 6.10.0 + resolution: "@octokit/webhooks-types@npm:6.10.0" + checksum: 6eac226f070a48d9ef2594df952e8540b001e2a8760c4fa5ac2f78c1750694a6c3b8de42fb4dbb4bb78335f091d9268539d4261c52a1a91fb9ef1e9af1959b44 languageName: node linkType: hard "@octokit/webhooks@npm:^10.0.0": - version: 10.5.1 - resolution: "@octokit/webhooks@npm:10.5.1" + version: 10.7.0 + resolution: "@octokit/webhooks@npm:10.7.0" dependencies: "@octokit/request-error": ^3.0.0 "@octokit/webhooks-methods": ^3.0.0 - "@octokit/webhooks-types": 6.7.0 + "@octokit/webhooks-types": 6.10.0 aggregate-error: ^3.1.0 - checksum: af8e4712fb1cbbda6ec5627586f9a7a5ee0c3bb6dc3af1875c42cdf3a1b10e959f9c05a5249722d9aac2ffefe8d572b03a77975f7cd9eacea114e01ebddb959a + checksum: c86d6ae97b59b540701b14e40aca88f47352cf4c03ee0237f714b65ea49b7416d262475d340ed0b4aee851c0a2fdc7d599982d30b4e5a3431986576fbcd4e86d languageName: node linkType: hard @@ -12369,28 +12829,28 @@ __metadata: linkType: hard "@opensearch-project/opensearch@npm:^2.0.0": - version: 2.1.0 - resolution: "@opensearch-project/opensearch@npm:2.1.0" + version: 2.2.0 + resolution: "@opensearch-project/opensearch@npm:2.2.0" dependencies: aws4: ^1.11.0 debug: ^4.3.1 - hpagent: ^0.1.1 + hpagent: ^1.2.0 ms: ^2.1.3 secure-json-parse: ^2.4.0 - checksum: 900b90f35d5b960fe60979168e21e9e5b6089a8e3dd9341b5d86c7f7aa7671cbfd937fbec311a6cfe195c113db755e699d3678ee0cbf454e9a9de1189d8cc935 + checksum: cceb5bb2c194a7d4bfab3c1b4a3230ea1457ae8976f39bb9e0c5e0067dc450a418a4649536988f0d48a746d7d3ed2002c32d9fde48dfc3112158e964bafa6e76 languageName: node linkType: hard "@opentelemetry/api@npm:^1.0.1, @opentelemetry/api@npm:^1.3.0": - version: 1.3.0 - resolution: "@opentelemetry/api@npm:1.3.0" - checksum: 33d284b67b6fab20ff72961d289c6487d3cb27caf7489f0231d7030551f82871e081e744b0390751d8aef3bf1614bd79f854788901a354e15274f552581fb374 + version: 1.4.0 + resolution: "@opentelemetry/api@npm:1.4.0" + checksum: 8dc522194e20d2e8aa6cac155dbce19d3fc9cfac59e953ece1064158c6348ccd9560ee99d2f2381e82c2f8c9a129b57fa7b640027383315504de1fa712b6d7f1 languageName: node linkType: hard "@oriflame/backstage-plugin-score-card@npm:^0.6.0": - version: 0.6.1 - resolution: "@oriflame/backstage-plugin-score-card@npm:0.6.1" + version: 0.6.2 + resolution: "@oriflame/backstage-plugin-score-card@npm:0.6.2" dependencies: "@backstage/catalog-model": ^1.1.3 "@backstage/config": ^1.0.4 @@ -12408,7 +12868,20 @@ __metadata: peerDependencies: react: ^16.13.1 || ^17.0.0 react-dom: ^16.13.1 || ^17.0.0 - checksum: 745e90ece212759f8d3c018b2ebe282e38529c11b13f1b8446ebc548b3a1c2a4a5d1219b8476f79e93ad0e9a919f6efcd372a27f1c3bab61e8e3165921abe8f0 + checksum: 6c8ca85981fde5af8e3951f0f5fc9c3873c890d286fd29067215246e9b8e9022ceea308c9caa8b0ccfe5380fe5640aaede48c9963a57c3c98f5c3b5feade9aae + languageName: node + linkType: hard + +"@parcel/watcher@npm:^2.1.0": + version: 2.1.0 + resolution: "@parcel/watcher@npm:2.1.0" + dependencies: + is-glob: ^4.0.3 + micromatch: ^4.0.5 + node-addon-api: ^3.2.1 + node-gyp: latest + node-gyp-build: ^4.3.0 + checksum: 17f512ad6d5dbb40053ceea7091f8af754afc63786b8f050b225b89a8ba24900468aad8bc4edb25c0349b4c0c8d061f50aa19242c0af52cbc30e6ebf50c7bf4c languageName: node linkType: hard @@ -12580,10 +13053,10 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.2.1": - version: 1.2.1 - resolution: "@remix-run/router@npm:1.2.1" - checksum: 71c947f83e130bf006226332aca395f626ad599b6d35dfe936f6a8f139cf37278bc1b36bce38c30f6062724402358296329f31c385b11ae59b13b76a2716d701 +"@remix-run/router@npm:1.3.2": + version: 1.3.2 + resolution: "@remix-run/router@npm:1.3.2" + checksum: ee2108b87d4a1241cdea137dd7e1741ee679228bd33fd81e22a6bb2940f81186cefe9a85e26d60cc49bbcc1bdbc57d1954b7d4d62f8a51ef69feddfc899f55fa languageName: node linkType: hard @@ -12594,18 +13067,18 @@ __metadata: languageName: node linkType: hard -"@rjsf/core-v5@npm:@rjsf/core@5.0.0-beta.16": - version: 5.0.0-beta.16 - resolution: "@rjsf/core@npm:5.0.0-beta.16" +"@rjsf/core-v5@npm:@rjsf/core@5.1.0": + version: 5.1.0 + resolution: "@rjsf/core@npm:5.1.0" dependencies: lodash: ^4.17.15 lodash-es: ^4.17.15 nanoid: ^3.3.4 prop-types: ^15.7.2 peerDependencies: - "@rjsf/utils": ^5.0.0-beta.1 + "@rjsf/utils": ^5.0.0 react: ^16.14.0 || >=17 - checksum: 38b0e02cfc394958d714f1c2b29262e95c5da01fbfffd657db7af8546ae5b00d1900fd0484be07be13d354dd1e48833cabcdcdaa8e9ffa0dfba63fba7e9556c3 + checksum: ff38046bb25de86b57e404c5d1ccea84b0485e916caf33a3cb25b030a38ed930a9b89a411671a7c29b047da38e0c682e20b43b13e61d77df770299385ba3c67a languageName: node linkType: hard @@ -12628,16 +13101,16 @@ __metadata: languageName: node linkType: hard -"@rjsf/material-ui-v5@npm:@rjsf/material-ui@5.0.0-beta.16": - version: 5.0.0-beta.16 - resolution: "@rjsf/material-ui@npm:5.0.0-beta.16" +"@rjsf/material-ui-v5@npm:@rjsf/material-ui@5.1.0": + version: 5.1.0 + resolution: "@rjsf/material-ui@npm:5.1.0" peerDependencies: "@material-ui/core": ^4.12.3 "@material-ui/icons": ^4.11.2 - "@rjsf/core": ^5.0.0-beta.1 - "@rjsf/utils": ^5.0.0-beta.1 + "@rjsf/core": ^5.0.0 + "@rjsf/utils": ^5.0.0 react: ^16.14.0 || >=17 - checksum: b8252dd7edd07435c065393cf4a86f7a8db0224646f71206561f9d300fb8d39a658282b5181f1b0100b069f775620953c03f89818fecdf34c52076e39108b93f + checksum: f8e2c486f33a60703cae5bb42ae82bc431b3d743c239868ca71907e7b4127a39d1688200820c0c8acdf3ecd3f5eaddef4ba47264695feead28e9b439d9be7ce8 languageName: node linkType: hard @@ -12653,9 +13126,9 @@ __metadata: languageName: node linkType: hard -"@rjsf/utils@npm:5.0.0-beta.16": - version: 5.0.0-beta.16 - resolution: "@rjsf/utils@npm:5.0.0-beta.16" +"@rjsf/utils@npm:5.1.0": + version: 5.1.0 + resolution: "@rjsf/utils@npm:5.1.0" dependencies: json-schema-merge-allof: ^0.8.1 jsonpointer: ^5.0.1 @@ -12664,31 +13137,32 @@ __metadata: react-is: ^18.2.0 peerDependencies: react: ^16.14.0 || >=17 - checksum: 7a0f04f20b1b12eff33322bef322ee96e222fc621142fb34906cb18386a2a0dd97224fd97851899644994a1adaab33c6f107076f344b66108219fcb809132703 + checksum: 76d7caf144f98b1547067bfdc2b1baecf452c63bd9d6f2cae6c162a99154bb4d2f5033f6abc0e26ab767dca9ea743f1f5a9d796696e90dd5ed1fdca5ee3c5280 languageName: node linkType: hard -"@rjsf/validator-ajv6@npm:5.0.0-beta.16": - version: 5.0.0-beta.16 - resolution: "@rjsf/validator-ajv6@npm:5.0.0-beta.16" +"@rjsf/validator-ajv8@npm:5.1.0": + version: 5.1.0 + resolution: "@rjsf/validator-ajv8@npm:5.1.0" dependencies: - ajv: ^6.7.0 + ajv: ^8.12.0 + ajv-formats: ^2.1.1 lodash: ^4.17.15 lodash-es: ^4.17.15 peerDependencies: - "@rjsf/utils": ^5.0.0-beta.1 - checksum: c4296e37608907c904424c46c884a15991101229e708caaa43e2e3c7d7e44a7e88ba206a902f80853f9a7f4c78d129fe47452ee5265cfd845b916051e99cd70d + "@rjsf/utils": ^5.0.0 + checksum: 3430a0aea8235deedd306b535ea3a62a9bcdd4dcf0bf1d022fe036041dceb5ca7c147b17ae50a0d25bb7c953729e987d96c68f5220f8fde4b56a89cf3f106a49 languageName: node linkType: hard "@roadiehq/backstage-plugin-buildkite@npm:^2.0.8": - version: 2.1.2 - resolution: "@roadiehq/backstage-plugin-buildkite@npm:2.1.2" + version: 2.1.5 + resolution: "@roadiehq/backstage-plugin-buildkite@npm:2.1.5" dependencies: - "@backstage/catalog-model": ^1.1.4 - "@backstage/core-components": ^0.12.2 - "@backstage/core-plugin-api": ^1.2.0 - "@backstage/plugin-catalog-react": ^1.2.3 + "@backstage/catalog-model": ^1.1.5 + "@backstage/core-components": ^0.12.3 + "@backstage/core-plugin-api": ^1.3.0 + "@backstage/plugin-catalog-react": ^1.2.4 "@backstage/theme": ^0.2.16 "@material-ui/core": ^4.12.1 "@material-ui/icons": ^4.11.2 @@ -12701,19 +13175,19 @@ __metadata: react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: fc607c7eac3a680db9ecc4c33bc65e84cdff432dc1079343f6e1ce66459324eba31a16a647b669c163e87c8b1794a01e66b4ba4f9fd77611d3081a0317cf68ac + checksum: d5ed0560401c621ae08b5eab82d5a9ebb36427f60ed2794071a7afcbc417c39c224438680234136aaaa992df19ed6b279b0292a03bdacac79359690d02256bcd languageName: node linkType: hard "@roadiehq/backstage-plugin-github-insights@npm:^2.0.5": - version: 2.3.2 - resolution: "@roadiehq/backstage-plugin-github-insights@npm:2.3.2" + version: 2.3.5 + resolution: "@roadiehq/backstage-plugin-github-insights@npm:2.3.5" dependencies: - "@backstage/catalog-model": ^1.1.4 - "@backstage/core-components": ^0.12.2 - "@backstage/core-plugin-api": ^1.2.0 - "@backstage/integration-react": ^1.1.8 - "@backstage/plugin-catalog-react": ^1.2.3 + "@backstage/catalog-model": ^1.1.5 + "@backstage/core-components": ^0.12.3 + "@backstage/core-plugin-api": ^1.3.0 + "@backstage/integration-react": ^1.1.9 + "@backstage/plugin-catalog-react": ^1.2.4 "@backstage/theme": ^0.2.16 "@date-io/core": 2.10.7 "@material-ui/core": ^4.11.0 @@ -12729,19 +13203,19 @@ __metadata: react: ^16.13.1 || ^17.0.0 react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 - checksum: 9c38cb944a9f37c3a0836dd4d5b41bc8329f124a026ce5f0c5ea467477ae87e6f699669c1c0873a9d29d791f6f870279bc7e83bc426111f540f8f6f54aad6f8a + checksum: 66e68cb85251df7be8d97e23810ab1d3b686ab99a76452e7daa8d4d3d44a0aee623defbbe83712601120af76d1d5a504fd62f6600edf6bfc623115d3a0f4ab00 languageName: node linkType: hard "@roadiehq/backstage-plugin-github-pull-requests@npm:^2.2.7": - version: 2.4.2 - resolution: "@roadiehq/backstage-plugin-github-pull-requests@npm:2.4.2" + version: 2.5.1 + resolution: "@roadiehq/backstage-plugin-github-pull-requests@npm:2.5.1" dependencies: - "@backstage/catalog-model": ^1.1.4 - "@backstage/core-components": ^0.12.2 - "@backstage/core-plugin-api": ^1.2.0 - "@backstage/plugin-catalog-react": ^1.2.3 - "@backstage/plugin-home": ^0.4.29 + "@backstage/catalog-model": ^1.1.5 + "@backstage/core-components": ^0.12.3 + "@backstage/core-plugin-api": ^1.3.0 + "@backstage/plugin-catalog-react": ^1.2.4 + "@backstage/plugin-home": ^0.4.30 "@material-ui/core": ^4.11.0 "@material-ui/icons": ^4.9.1 "@material-ui/lab": ^4.0.0-alpha.60 @@ -12751,25 +13225,25 @@ __metadata: history: ^5.0.0 lodash: ^4.17.21 moment: ^2.27.0 - msw: ^0.39.2 + msw: ^1.0.1 node-fetch: ^2.6.1 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 - checksum: d2b80b4c25a0bdca2e6dcd8ddf2fc26aa75374d5278fc235551fb213f3c0269e81cb141afd98c3a2a3073c39cfe35db69b1c265a615dd880777ab2e6c6a69d22 + checksum: 769991ac46d4466261f2765eaab6eca903e751dc003d94a21dfb0376fca69af8edb2a8b9b12b375e903694278d32a95f8745d1f724a19429df34adeb3fb3e58c languageName: node linkType: hard "@roadiehq/backstage-plugin-travis-ci@npm:^2.0.5": - version: 2.1.2 - resolution: "@roadiehq/backstage-plugin-travis-ci@npm:2.1.2" + version: 2.1.5 + resolution: "@roadiehq/backstage-plugin-travis-ci@npm:2.1.5" dependencies: - "@backstage/catalog-model": ^1.1.4 - "@backstage/core-components": ^0.12.2 - "@backstage/core-plugin-api": ^1.2.0 - "@backstage/plugin-catalog-react": ^1.2.3 + "@backstage/catalog-model": ^1.1.5 + "@backstage/core-components": ^0.12.3 + "@backstage/core-plugin-api": ^1.3.0 + "@backstage/plugin-catalog-react": ^1.2.4 "@backstage/theme": ^0.2.16 "@material-ui/core": ^4.11.3 "@material-ui/icons": ^4.11.2 @@ -12784,7 +13258,7 @@ __metadata: react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: 64e2878fd517e63eb765a65c60f1276cc6a12e4abb128001ed92a1e634546c0ec9236cbb67c6b534ece126fe64b5fb251e985fe3e5f42ce4500b6108b9f7f889 + checksum: 955679aafcc707ca8d0b68eb394736209e13a1dee34fc4e0ff438f64568bcdf225ff5f68f389d89880a27debe06c3e7ae343f6435b942e7f4bda64f90285c4be languageName: node linkType: hard @@ -12968,9 +13442,9 @@ __metadata: linkType: hard "@sideway/formula@npm:^3.0.0": - version: 3.0.0 - resolution: "@sideway/formula@npm:3.0.0" - checksum: 8ae26a0ed6bc84f7310be6aae6eb9d81e97f382619fc69025d346871a707eaab0fa38b8c857e3f0c35a19923de129f42d35c50b8010c928d64aab41578580ec4 + version: 3.0.1 + resolution: "@sideway/formula@npm:3.0.1" + checksum: e4beeebc9dbe2ff4ef0def15cec0165e00d1612e3d7cea0bc9ce5175c3263fc2c818b679bd558957f49400ee7be9d4e5ac90487e1625b4932e15c4aa7919c57a languageName: node linkType: hard @@ -13284,90 +13758,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-darwin-arm64@npm:1.3.26" +"@swc/core-darwin-arm64@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-darwin-arm64@npm:1.3.36" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-darwin-x64@npm:1.3.26" +"@swc/core-darwin-x64@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-darwin-x64@npm:1.3.36" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.26" +"@swc/core-linux-arm-gnueabihf@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.36" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.26" +"@swc/core-linux-arm64-gnu@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.36" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.26" +"@swc/core-linux-arm64-musl@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.36" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.26" +"@swc/core-linux-x64-gnu@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.36" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-linux-x64-musl@npm:1.3.26" +"@swc/core-linux-x64-musl@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-linux-x64-musl@npm:1.3.36" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.26" +"@swc/core-win32-arm64-msvc@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.36" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.26" +"@swc/core-win32-ia32-msvc@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.36" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.26": - version: 1.3.26 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.26" +"@swc/core-win32-x64-msvc@npm:1.3.36": + version: 1.3.36 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.36" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.3.9": - version: 1.3.26 - resolution: "@swc/core@npm:1.3.26" + version: 1.3.36 + resolution: "@swc/core@npm:1.3.36" dependencies: - "@swc/core-darwin-arm64": 1.3.26 - "@swc/core-darwin-x64": 1.3.26 - "@swc/core-linux-arm-gnueabihf": 1.3.26 - "@swc/core-linux-arm64-gnu": 1.3.26 - "@swc/core-linux-arm64-musl": 1.3.26 - "@swc/core-linux-x64-gnu": 1.3.26 - "@swc/core-linux-x64-musl": 1.3.26 - "@swc/core-win32-arm64-msvc": 1.3.26 - "@swc/core-win32-ia32-msvc": 1.3.26 - "@swc/core-win32-x64-msvc": 1.3.26 + "@swc/core-darwin-arm64": 1.3.36 + "@swc/core-darwin-x64": 1.3.36 + "@swc/core-linux-arm-gnueabihf": 1.3.36 + "@swc/core-linux-arm64-gnu": 1.3.36 + "@swc/core-linux-arm64-musl": 1.3.36 + "@swc/core-linux-x64-gnu": 1.3.36 + "@swc/core-linux-x64-musl": 1.3.36 + "@swc/core-win32-arm64-msvc": 1.3.36 + "@swc/core-win32-ia32-msvc": 1.3.36 + "@swc/core-win32-x64-msvc": 1.3.36 dependenciesMeta: "@swc/core-darwin-arm64": optional: true @@ -13389,7 +13863,7 @@ __metadata: optional: true "@swc/core-win32-x64-msvc": optional: true - checksum: 656a2197bd8bd50b112077e8c7f9156527ec66cb06fed2cefb9e2610e3cde13e8416783f84edda67b9af4e27d6b169bc1a447d7c6c73cf64a1b3ec428b0737ab + checksum: 36e96e86d1ab8fd84eb51e7264a7ae781ee7e507d36b9ccc028d7f31a2ccf2949f074656e34139c970196774d24d55a54c3aa5c53d04eb6e3569c71cbfed0c0d languageName: node linkType: hard @@ -13423,18 +13897,18 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:4.22.0": - version: 4.22.0 - resolution: "@tanstack/query-core@npm:4.22.0" - checksum: d24c13f8e21b65380230055a6673ae670aca5ee550c2f9a24b89fd3ac5243b465bb25f61b29da8ca285fc7fb0f4c2f8add3864f742eafac5131e777f3e52767a +"@tanstack/query-core@npm:4.24.10": + version: 4.24.10 + resolution: "@tanstack/query-core@npm:4.24.10" + checksum: 8268b9dca4a2ed39f8575976d8a3e2f839bb6f4fb821dc00ba8e2aeffffe2f73e23344bcc24b9cb6dae9dbfe3ba2b14c76a1e0a38b4064c83beb9aa607fd94bc languageName: node linkType: hard "@tanstack/react-query@npm:^4.1.3": - version: 4.22.0 - resolution: "@tanstack/react-query@npm:4.22.0" + version: 4.24.10 + resolution: "@tanstack/react-query@npm:4.24.10" dependencies: - "@tanstack/query-core": 4.22.0 + "@tanstack/query-core": 4.24.10 use-sync-external-store: ^1.2.0 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -13445,7 +13919,7 @@ __metadata: optional: true react-native: optional: true - checksum: b146774b44a79dd5a6412d981ce04790e66da5a36f907a03c85c36f50a38fb4e4dfbfca7b1561c86b39fb0df1c0c1ae0a0ef2930c06b444da3f430b9bad450f1 + checksum: 576a2c73e98cf6a3c9584283989383d5ac63b01efe546cc7580ec7b1edf0c0cb8958a3071778cb0af908f5269d45c7516fa388e5f856e37f5b2d3c6110974cbe languageName: node linkType: hard @@ -13459,6 +13933,7 @@ __metadata: "@backstage/cli-common": "workspace:^" "@backstage/config": "workspace:^" "@backstage/plugin-techdocs-node": "workspace:^" + "@frsource/cypress-plugin-visual-regression-diff": ^3.2.8 "@types/commander": ^2.12.2 "@types/dockerode": ^3.3.0 "@types/fs-extra": ^9.0.6 @@ -13468,7 +13943,6 @@ __metadata: "@types/webpack-env": ^1.15.3 commander: ^9.1.0 cypress: ^10.0.0 - cypress-plugin-snapshots: ^1.4.4 dockerode: ^3.3.1 find-process: ^1.4.5 fs-extra: ^10.0.1 @@ -13529,7 +14003,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/react-hooks@npm:^8.0.0": +"@testing-library/react-hooks@npm:^8.0.0, @testing-library/react-hooks@npm:^8.0.1": version: 8.0.1 resolution: "@testing-library/react-hooks@npm:8.0.1" dependencies: @@ -13816,11 +14290,11 @@ __metadata: linkType: hard "@types/codemirror@npm:^5.0.0": - version: 5.60.6 - resolution: "@types/codemirror@npm:5.60.6" + version: 5.60.7 + resolution: "@types/codemirror@npm:5.60.7" dependencies: "@types/tern": "*" - checksum: 10b8c2a689f9ed610b0048d9d5ded1a7eb90c45014156b84b41c4f52f4720172414f7172d56f13c8a0b35d429704fb9d240414cf205776c807caa39105b33481 + checksum: 0312ce032eb139b408588a7e7cf3b392a510a2cfa6f477f2dd20d8bca1990e5e6e5540e708f9a69d1a58e68ea7fd234b789c801aa7ef4f3b3dc32a1478b04a91 languageName: node linkType: hard @@ -13958,6 +14432,15 @@ __metadata: languageName: node linkType: hard +"@types/cross-spawn@npm:^6.0.2": + version: 6.0.2 + resolution: "@types/cross-spawn@npm:6.0.2" + dependencies: + "@types/node": "*" + checksum: fa9edd32178878cab3ea8d6d0260639e0fe4860ddb3887b8de53d6e8036e154fc5f313c653f690975aa25025aea8beb83fb0870b931bf8d9202c3ac530a24c9d + languageName: node + linkType: hard + "@types/d3-color@npm:*": version: 3.0.2 resolution: "@types/d3-color@npm:3.0.2" @@ -14172,9 +14655,9 @@ __metadata: linkType: hard "@types/event-source-polyfill@npm:^1.0.0": - version: 1.0.0 - resolution: "@types/event-source-polyfill@npm:1.0.0" - checksum: 5b3eacace596ce8c1c3f877ea7967e9634922faa7187f9d4000fd3d7b765f84917603dbc95c98d28ff592e80cb7bc588166ec9ea841d262be0d919bf9ed79c14 + version: 1.0.1 + resolution: "@types/event-source-polyfill@npm:1.0.1" + checksum: 37f3ccc40e2fc43230047c3323f8bfc55f6ed40aee8c61af7f6b702a923be5615362cf3241f5a536f9c64932309a2184a0181bb48e105a94b968b8321cae1c60 languageName: node linkType: hard @@ -14186,13 +14669,13 @@ __metadata: linkType: hard "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.18, @types/express-serve-static-core@npm:^4.17.31, @types/express-serve-static-core@npm:^4.17.5": - version: 4.17.32 - resolution: "@types/express-serve-static-core@npm:4.17.32" + version: 4.17.33 + resolution: "@types/express-serve-static-core@npm:4.17.33" dependencies: "@types/node": "*" "@types/qs": "*" "@types/range-parser": "*" - checksum: 70ec1b8f386628850b315a7b9fd4240a5a70297b41ef1c39af65c8b9661d2c775cfff4686b491fd90e5b6eef43088af203700c5541aec0d063db0c6cbeff254c + checksum: dce580d16b85f207445af9d4053d66942b27d0c72e86153089fa00feee3e96ae336b7bedb31ed4eea9e553c99d6dd356ed6e0928f135375d9f862a1a8015adf2 languageName: node linkType: hard @@ -14208,11 +14691,11 @@ __metadata: linkType: hard "@types/express-session@npm:^1.17.2": - version: 1.17.5 - resolution: "@types/express-session@npm:1.17.5" + version: 1.17.6 + resolution: "@types/express-session@npm:1.17.6" dependencies: "@types/express": "*" - checksum: f6995f7720a18546bcb10cc707cf8c9c92455eb4319c0c50c57e9b9b10ed20dc379d74d5e4c3323fe4e924238b75e13c97822c9d81a80fa064ddc71654c1059d + checksum: f5f2201d6b9568d8b9319735a4f6806b5ca144cee450274965aec54bc0a0331ba8bfffc1ad5addadf2e625769e13813a48cb5aea5b166fa05f474a12302dbb38 languageName: node linkType: hard @@ -14226,15 +14709,15 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.17.15, @types/express@npm:^4.17.6": - version: 4.17.15 - resolution: "@types/express@npm:4.17.15" +"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.17.6": + version: 4.17.16 + resolution: "@types/express@npm:4.17.16" dependencies: "@types/body-parser": "*" "@types/express-serve-static-core": ^4.17.31 "@types/qs": "*" "@types/serve-static": "*" - checksum: b4acd8a836d4f6409cdf79b12d6e660485249b62500cccd61e7997d2f520093edf77d7f8498ca79d64a112c6434b6de5ca48039b8fde2c881679eced7e96979b + checksum: 43f3ed2cea6e5e83c7c1098c5152f644e975fd764443717ff9c812a1518416a9e7e9f824ffe852c118888cbfb994ed023cad08331f49b19ced469bb185cdd5cd languageName: node linkType: hard @@ -14360,11 +14843,11 @@ __metadata: linkType: hard "@types/http-proxy@npm:*, @types/http-proxy@npm:^1.17.4, @types/http-proxy@npm:^1.17.8": - version: 1.17.9 - resolution: "@types/http-proxy@npm:1.17.9" + version: 1.17.10 + resolution: "@types/http-proxy@npm:1.17.10" dependencies: "@types/node": "*" - checksum: 7a6746d00729b2a9fe9f9dd3453430b099931df879ec8f7a7b5f07b1795f6d99b0512640c45a67390b1e4bacb9401e36824952aeeaf089feba8627a063cf8e00 + checksum: 8fabee5d01715e338f426715325121d6c4b7a9694dee716ab61c874e0aaccee9a0fff7ccc3c9d7e37a8feeaab7c783c17aaa9943efbc8849c5e79ecd7eaf02ab languageName: node linkType: hard @@ -14376,11 +14859,12 @@ __metadata: linkType: hard "@types/inquirer@npm:^8.1.3": - version: 8.2.5 - resolution: "@types/inquirer@npm:8.2.5" + version: 8.2.6 + resolution: "@types/inquirer@npm:8.2.6" dependencies: "@types/through": "*" - checksum: 932c432e634697bcff5d50fdc9e64f90d2e31c5ebcda909f2e9704d0433b5ec608b6ece985c6e57a283f3b62434f1cd3619b64ca61433d7c3bdb41d3c5f27586 + rxjs: ^7.2.0 + checksum: d09c3b6bbfb1aff8bdb8fc938d43536be55dfb51af0c91d0105e3f7c5e3950c12618ac00e2e91f10b5abeba38f4b903289be1df5aa31f213b58c3cc675a19f81 languageName: node linkType: hard @@ -14444,12 +14928,12 @@ __metadata: linkType: hard "@types/jest@npm:*, @types/jest@npm:^29.0.0": - version: 29.2.5 - resolution: "@types/jest@npm:29.2.5" + version: 29.2.6 + resolution: "@types/jest@npm:29.2.6" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: d668470f00ec4cb8b8457f1fd90f7358fad8f22d74b85006dad6be522d6b9bf10f49f597e88d1d1a518d211c1b65be32a1f27f0e49ce0658d110a9206b8ea310 + checksum: 90190ac830334af1470d255853f9621fe657e5030b4d96773fc1f884833cd303c76580b00c1b86dc38a8db94f1c7141d462190437a10af31852b8845a57c48ba languageName: node linkType: hard @@ -14523,7 +15007,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.11 resolution: "@types/json-schema@npm:7.0.11" checksum: 527bddfe62db9012fccd7627794bd4c71beb77601861055d87e3ee464f2217c85fca7a4b56ae677478367bbd248dbde13553312b7d4dbc702a2f2bbf60c4018d @@ -14562,6 +15046,16 @@ __metadata: languageName: node linkType: hard +"@types/jss@npm:^9.5.6": + version: 9.5.8 + resolution: "@types/jss@npm:9.5.8" + dependencies: + csstype: ^2.0.0 + indefinite-observable: ^1.0.1 + checksum: 6e51707529a15f2f5ff94555ecb555d29427fd10c4f3d2d29474934292e365c2dfaa4ad30b2ab46946201cbca7f6e8df56513a22fc6ceca10a979db8338935c5 + languageName: node + linkType: hard + "@types/jwt-decode@npm:^3.1.0": version: 3.1.0 resolution: "@types/jwt-decode@npm:3.1.0" @@ -14753,10 +15247,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>= 8, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": - version: 18.11.18 - resolution: "@types/node@npm:18.11.18" - checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d +"@types/node@npm:*, @types/node@npm:>= 8, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^18.11.17": + version: 18.11.19 + resolution: "@types/node@npm:18.11.19" + checksum: d7cd19fcfc59cbdd3f9ba0b4072cb7adc21bd575bd8eb7d7e698975e63564aaa83f03434f32b12331f84f73d0b369d9cbe2371e359d9d7f5c3361f4987f4f7da languageName: node linkType: hard @@ -14767,7 +15261,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^10.1.0, @types/node@npm:^10.12.0": +"@types/node@npm:^10.1.0": version: 10.17.60 resolution: "@types/node@npm:10.17.60" checksum: 2cdb3a77d071ba8513e5e8306fa64bf50e3c3302390feeaeff1fd325dd25c8441369715dfc8e3701011a72fed5958c7dfa94eb9239a81b3c286caa4d97db6eef @@ -14796,9 +15290,9 @@ __metadata: linkType: hard "@types/node@npm:^16.0.0, @types/node@npm:^16.11.26, @types/node@npm:^16.9.2": - version: 16.18.11 - resolution: "@types/node@npm:16.18.11" - checksum: 2a3b1da13063debe6e26f732defb5f03ef4ef732c3e08daba838d8850433bd00e537ce1a97ce9bcfc4b15db5218d701d1265fae94e0d6926906bec157e6b46e0 + version: 16.18.12 + resolution: "@types/node@npm:16.18.12" + checksum: fc3271182414f8593018ef8f00b4718116a92f463f619081bd399d9460e7861e1dd7eebc7cf94c23567e418ff397babed077011711aae8d47171b5a81c5bd71d languageName: node linkType: hard @@ -14817,9 +15311,9 @@ __metadata: linkType: hard "@types/nunjucks@npm:^3.1.4": - version: 3.2.1 - resolution: "@types/nunjucks@npm:3.2.1" - checksum: 40f3dd8968cd30c19a99c6eec55e3bf354da39ce08840a8d652a7c9754a7e5a8a59f92fb258862c56d223f9908b62c347006d0a26f685a5dc72f13bdb68ce778 + version: 3.2.2 + resolution: "@types/nunjucks@npm:3.2.2" + checksum: e0fdc68f01635b34ea6f5e8845ed6d2e8d11be6347b2f28c8093636454e2401f02c26a2ad6aa459da56f2459f2d59bf72914b6e4774bcd058fa12e2ae5f0f6ef languageName: node linkType: hard @@ -14871,12 +15365,12 @@ __metadata: languageName: node linkType: hard -"@types/passport-microsoft@npm:^0.0.0": - version: 0.0.0 - resolution: "@types/passport-microsoft@npm:0.0.0" +"@types/passport-microsoft@npm:^1.0.0": + version: 1.0.0 + resolution: "@types/passport-microsoft@npm:1.0.0" dependencies: "@types/passport-oauth2": "*" - checksum: fee27b238975ba86982616d713247bd9f8ab4e325270dd42841c140710ece3f32655b2536fcbffe0166f25b51e1b25316006718f037b5a88018406c64351ae27 + checksum: d7c8b439b18cddcf707ea0c575748ecb344b6fb3c78a847aaad83246abf081c8b485efa86a3788cd5f49f4ca84a1444d813697a433fab9d04f35fb67d628cb23 languageName: node linkType: hard @@ -14912,11 +15406,11 @@ __metadata: linkType: hard "@types/passport@npm:*, @types/passport@npm:^1.0.3": - version: 1.0.11 - resolution: "@types/passport@npm:1.0.11" + version: 1.0.12 + resolution: "@types/passport@npm:1.0.12" dependencies: "@types/express": "*" - checksum: 7fea92aeb0d05bd872c17deadc1f1219dbdb23bae1b8f50e1c432ef755bce2b5f3fd60af76c38815f1fbaa047960d3638a4144221c4330457091be1f6e492168 + checksum: 2bce41b248566d8c2aeafcb38f105e3521d42a572e92da9edc5eb669f1b3cb02e5911c9d124191f67d432b677562a885b6ad7f56320c712df1e87ba7619eb8a4 languageName: node linkType: hard @@ -15021,6 +15515,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^2.0.8": + version: 2.9.2 + resolution: "@types/react-transition-group@npm:2.9.2" + dependencies: + "@types/react": "*" + checksum: 6f30fffc221339de90bd3e999f32328618cfaedfcaa501603f5ddc5bed1c2dbaa0d51347c3a25e5f8fd3041c671aabd2b7bfbcad611a7636adcd9da4e0666fa5 + languageName: node + linkType: hard + "@types/react-transition-group@npm:^4.2.0": version: 4.2.4 resolution: "@types/react-transition-group@npm:4.2.4" @@ -15129,6 +15632,16 @@ __metadata: languageName: node linkType: hard +"@types/rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "@types/rimraf@npm:3.0.2" + dependencies: + "@types/glob": "*" + "@types/node": "*" + checksum: b47fa302f46434cba704d20465861ad250df79467d3d289f9d6490d3aeeb41e8cb32dd80bd1a8fd833d1e185ac719fbf9be12e05ad9ce9be094d8ee8f1405347 + languageName: node + linkType: hard + "@types/rollup-plugin-peer-deps-external@npm:^2.2.0": version: 2.2.1 resolution: "@types/rollup-plugin-peer-deps-external@npm:2.2.1" @@ -15336,12 +15849,12 @@ __metadata: linkType: hard "@types/tar@npm:^6.1.1": - version: 6.1.3 - resolution: "@types/tar@npm:6.1.3" + version: 6.1.4 + resolution: "@types/tar@npm:6.1.4" dependencies: "@types/node": "*" - minipass: ^3.3.5 - checksum: 3a221f74adfcef8555b9c4cc951907dfd567630744ffe5211da1b44caf2a4c3b02297b73c9fb02d171e93a7a7c74fb15c1826e3f0438f0e5e8f4c790db59ddcf + minipass: ^4.0.0 + checksum: 48a1f6b8665ed03f37f74966cfe991a33d6e85cee28aaa6ef42c7ca8448cdbc1e2ca96b473b6b53f1d7bd91423ae0dcfe54a295607d7ac4b4cbb9dbf167df7fa languageName: node linkType: hard @@ -15388,6 +15901,13 @@ __metadata: languageName: node linkType: hard +"@types/triple-beam@npm:^1.3.2": + version: 1.3.2 + resolution: "@types/triple-beam@npm:1.3.2" + checksum: dd7b4a563fb710abc992e5d59eac481bed9e303fada2e276e37b00be31c392e03300ee468e57761e616512872e77935f92472877d0704a19688d15a726cee17b + languageName: node + linkType: hard + "@types/trusted-types@npm:*": version: 2.0.2 resolution: "@types/trusted-types@npm:2.0.2" @@ -15453,21 +15973,12 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^6.0.1": - version: 6.0.4 - resolution: "@types/ws@npm:6.0.4" +"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.1, @types/ws@npm:^8.5.3": + version: 8.5.4 + resolution: "@types/ws@npm:8.5.4" dependencies: "@types/node": "*" - checksum: b2656a76bfad0c17bb1e3fc237ba7122431c1373669977ed8edef45934c82f71c75d8c71f0a576dc6d98b0954fd94cae0166c6b4ccb40f7e0ee29cc92673519c - languageName: node - linkType: hard - -"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.1": - version: 8.5.3 - resolution: "@types/ws@npm:8.5.3" - dependencies: - "@types/node": "*" - checksum: 0ce46f850d41383fcdc2149bcacc86d7232fa7a233f903d2246dff86e31701a02f8566f40af5f8b56d1834779255c04ec6ec78660fe0f9b2a69cf3d71937e4ae + checksum: fefbad20d211929bb996285c4e6f699b12192548afedbe4930ab4384f8a94577c9cd421acaad163cacd36b88649509970a05a0b8f20615b30c501ed5269038d1 languageName: node linkType: hard @@ -15536,13 +16047,14 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^5.9.0": - version: 5.48.1 - resolution: "@typescript-eslint/eslint-plugin@npm:5.48.1" + version: 5.53.0 + resolution: "@typescript-eslint/eslint-plugin@npm:5.53.0" dependencies: - "@typescript-eslint/scope-manager": 5.48.1 - "@typescript-eslint/type-utils": 5.48.1 - "@typescript-eslint/utils": 5.48.1 + "@typescript-eslint/scope-manager": 5.53.0 + "@typescript-eslint/type-utils": 5.53.0 + "@typescript-eslint/utils": 5.53.0 debug: ^4.3.4 + grapheme-splitter: ^1.0.4 ignore: ^5.2.0 natural-compare-lite: ^1.4.0 regexpp: ^3.2.0 @@ -15554,7 +16066,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: d8d73d123d16fc9b50b500ef21816dcabdffe0d2dcfdb15089dc5a1015d57cbad709de565d1c830f5058c0d7b410069e2554c0b53d1485fe7b237ea8089e58be + checksum: 12dffe65969d8e5248c86a700fe46a737e55ecafb276933e747b4731eab6266fe55e2d43a34b8b340179fe248e127d861cd016a7614b1b9804cd0687c99616d1 languageName: node linkType: hard @@ -15575,29 +16087,29 @@ __metadata: linkType: hard "@typescript-eslint/parser@npm:^5.9.0": - version: 5.48.1 - resolution: "@typescript-eslint/parser@npm:5.48.1" + version: 5.53.0 + resolution: "@typescript-eslint/parser@npm:5.53.0" dependencies: - "@typescript-eslint/scope-manager": 5.48.1 - "@typescript-eslint/types": 5.48.1 - "@typescript-eslint/typescript-estree": 5.48.1 + "@typescript-eslint/scope-manager": 5.53.0 + "@typescript-eslint/types": 5.53.0 + "@typescript-eslint/typescript-estree": 5.53.0 debug: ^4.3.4 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: c624d24eb209b4ce7f0a6c8116712363f10a9c9a5138f240e254ff265526ee4b0fd73b7b6b4b6a0e7611bd9934c42036350dd27f96ae2fa4efdade1a7ebd0e9e + checksum: 979e5d63793a9e64998b1f956ba0f00f8a2674db3a664fafce7b2433323f5248bd776af8305e2419d73a9d94c55176fee099abc5c153b4cc52e5765c725c1edd languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.48.1": - version: 5.48.1 - resolution: "@typescript-eslint/scope-manager@npm:5.48.1" +"@typescript-eslint/scope-manager@npm:5.53.0": + version: 5.53.0 + resolution: "@typescript-eslint/scope-manager@npm:5.53.0" dependencies: - "@typescript-eslint/types": 5.48.1 - "@typescript-eslint/visitor-keys": 5.48.1 - checksum: f60a7efe917798cccf8652925de6be58b023ded6c6ee44ce74d074f0c2a1927680398a6d73bab33d500c69474ad8c54d63b90fcc6e13256712707d12a60e0a64 + "@typescript-eslint/types": 5.53.0 + "@typescript-eslint/visitor-keys": 5.53.0 + checksum: 51f31dc01e95908611f402441f58404da80a338c0237b2b82f4a7b0b2e8868c4bfe8f7cf44b2567dd56533de609156a5d4ac54bb1f9f09c7014b99428aef2543 languageName: node linkType: hard @@ -15611,12 +16123,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.48.1": - version: 5.48.1 - resolution: "@typescript-eslint/type-utils@npm:5.48.1" +"@typescript-eslint/type-utils@npm:5.53.0": + version: 5.53.0 + resolution: "@typescript-eslint/type-utils@npm:5.53.0" dependencies: - "@typescript-eslint/typescript-estree": 5.48.1 - "@typescript-eslint/utils": 5.48.1 + "@typescript-eslint/typescript-estree": 5.53.0 + "@typescript-eslint/utils": 5.53.0 debug: ^4.3.4 tsutils: ^3.21.0 peerDependencies: @@ -15624,14 +16136,14 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 2739b35caf48c9edbeab82936de58ce0759ab34955ce7eec1786690d6a63146ae0a6c5d9c76034605d9fe200c87a73ede0772c6244c5df6e66df992d9ebbab72 + checksum: 52c40967c5fabd58c2ae8bf519ef89e4feb511e4df630aeaeac8335661a79b6b3a32d30a61a5f1d8acc703f21c4d90751a5d41cda1b35d08867524da11bc2e1d languageName: node linkType: hard -"@typescript-eslint/types@npm:5.48.1": - version: 5.48.1 - resolution: "@typescript-eslint/types@npm:5.48.1" - checksum: 8437986e9d86d792b23327517ae2f9861ec55992d5a9cd55991e525409b6244169436cd708f3987ab7c579e45e59b6eab5a9d3583f7729219e25691164293094 +"@typescript-eslint/types@npm:5.53.0": + version: 5.53.0 + resolution: "@typescript-eslint/types@npm:5.53.0" + checksum: b0eaf23de4ab13697d4d2095838c959a3f410c30f0d19091e5ca08e62320c3cc3c72bcb631823fb6a4fbb31db0a059e386a0801244930d0a88a6a698e5f46548 languageName: node linkType: hard @@ -15642,12 +16154,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.48.1": - version: 5.48.1 - resolution: "@typescript-eslint/typescript-estree@npm:5.48.1" +"@typescript-eslint/typescript-estree@npm:5.53.0": + version: 5.53.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.53.0" dependencies: - "@typescript-eslint/types": 5.48.1 - "@typescript-eslint/visitor-keys": 5.48.1 + "@typescript-eslint/types": 5.53.0 + "@typescript-eslint/visitor-keys": 5.53.0 debug: ^4.3.4 globby: ^11.1.0 is-glob: ^4.0.3 @@ -15656,7 +16168,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 2b26e5848ef131e1bb99ed54d8c0efa8279cf8e8f7d8b72de00c2ca6cf2799d96c20f5bbbcf26e14e81b7b9d1035ba509bff30f2d852c174815879e8f14c27ed + checksum: 6e119c8e4167c8495d728c5556a834545a9c064918dd5e7b79b0d836726f4f8e2a0297b0ac82bf2b71f1e5427552217d0b59d8fb1406fd79bd3bf91b75dca873 languageName: node linkType: hard @@ -15678,31 +16190,31 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.48.1, @typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.13.0": - version: 5.48.1 - resolution: "@typescript-eslint/utils@npm:5.48.1" +"@typescript-eslint/utils@npm:5.53.0, @typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.43.0": + version: 5.53.0 + resolution: "@typescript-eslint/utils@npm:5.53.0" dependencies: "@types/json-schema": ^7.0.9 "@types/semver": ^7.3.12 - "@typescript-eslint/scope-manager": 5.48.1 - "@typescript-eslint/types": 5.48.1 - "@typescript-eslint/typescript-estree": 5.48.1 + "@typescript-eslint/scope-manager": 5.53.0 + "@typescript-eslint/types": 5.53.0 + "@typescript-eslint/typescript-estree": 5.53.0 eslint-scope: ^5.1.1 eslint-utils: ^3.0.0 semver: ^7.3.7 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 2d112cbb6a920f147c6c3322e404ca3c56c1170e1ede3bcbf16fb779960dc24cdba688b1f2d06acd242859fc1dbc8702da5f8fa8bbf53e7081e41d80bec4c236 + checksum: 18e6bac14ae853385a74123759850bca367904723e170c37416fc014673eb714afb6bb090367bff61494a8387e941b6af65ee5f4f845f7177fabb4df85e01643 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.48.1": - version: 5.48.1 - resolution: "@typescript-eslint/visitor-keys@npm:5.48.1" +"@typescript-eslint/visitor-keys@npm:5.53.0": + version: 5.53.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.53.0" dependencies: - "@typescript-eslint/types": 5.48.1 + "@typescript-eslint/types": 5.53.0 eslint-visitor-keys: ^3.3.0 - checksum: 2bda10cf4e6bc48b0d463767617e48a832d708b9434665dff6ed101f7d33e0d592f02af17a2259bde1bd17e666246448ae78d0fe006148cb93d897fff9b1d134 + checksum: 090695883c15364c6f401e97f56b13db0f31c1114f3bd22562bd41734864d27f6a3c80de33957e9dedab2d5f94b0f4480ba3fde1d4574e74dca4593917b7b54a languageName: node linkType: hard @@ -15716,9 +16228,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.19.6": - version: 4.19.6 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.19.6" +"@uiw/codemirror-extensions-basic-setup@npm:4.19.9": + version: 4.19.9 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.19.9" dependencies: "@codemirror/autocomplete": ^6.0.0 "@codemirror/commands": ^6.0.0 @@ -15735,19 +16247,19 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 775c6c190b27ed0ffeb9fec9b03c60a4d621f33480e7c878c3841cb0fa983baab46d13612d7ac9a14a3e810b83d9ab12aa387a291e5e5a0726bf33f28d89d1c0 + checksum: f248d1623f33b88b61dd75955441d1b5925b2b2c78d595eac1d4603dc31f107d5973d9cff42c3ade4a2493828d679a8908e5dad03a570511e6095d0028089f85 languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.9.3": - version: 4.19.6 - resolution: "@uiw/react-codemirror@npm:4.19.6" + version: 4.19.9 + resolution: "@uiw/react-codemirror@npm:4.19.9" dependencies: "@babel/runtime": ^7.18.6 "@codemirror/commands": ^6.1.0 "@codemirror/state": ^6.1.1 "@codemirror/theme-one-dark": ^6.0.0 - "@uiw/codemirror-extensions-basic-setup": 4.19.6 + "@uiw/codemirror-extensions-basic-setup": 4.19.9 codemirror: ^6.0.0 peerDependencies: "@babel/runtime": ">=7.11.0" @@ -15757,7 +16269,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 64c0374dd221e7ddab2f8c2531ad5271f595fad116e37b45acaa9f1d8b8d5659bccdd4aea47a558cce4c62d46e90b8d074760a5b328e9e99c253c79ec99e8b6a + checksum: ed0d9a7b927df718535bccf553fa7cdabd2cb776f74e6ae75eab0ba321bc60468b5f1b8576a12543729770a11e5438fd4fc925f3f74643985c34a2184c5ae7f6 languageName: node linkType: hard @@ -15912,6 +16424,13 @@ __metadata: languageName: node linkType: hard +"@whatwg-node/events@npm:^0.0.2": + version: 0.0.2 + resolution: "@whatwg-node/events@npm:0.0.2" + checksum: 6d491801d36967d5d62443cca6ef39d41cf882d575839d2e9d1a5f394722cd24ef84fe9e897bb72c01bd198871fda9ff0e8b1ac5aa6f3f814f87d92b7f28fdcc + languageName: node + linkType: hard + "@whatwg-node/fetch@npm:0.5.3, @whatwg-node/fetch@npm:^0.5.0": version: 0.5.3 resolution: "@whatwg-node/fetch@npm:0.5.3" @@ -15945,20 +16464,31 @@ __metadata: languageName: node linkType: hard -"@whatwg-node/fetch@npm:^0.6.0": - version: 0.6.1 - resolution: "@whatwg-node/fetch@npm:0.6.1" +"@whatwg-node/fetch@npm:^0.8.0": + version: 0.8.1 + resolution: "@whatwg-node/fetch@npm:0.8.1" dependencies: "@peculiar/webcrypto": ^1.4.0 - abort-controller: ^3.0.0 + "@whatwg-node/node-fetch": ^0.3.0 busboy: ^1.6.0 - form-data-encoder: ^1.7.1 - formdata-node: ^4.3.1 - node-fetch: ^2.6.7 - undici: ^5.12.0 urlpattern-polyfill: ^6.0.2 - web-streams-polyfill: ^3.2.0 - checksum: 81b469c03e53f49f6a1a2d8059ba28d40b710611e01a60f6396cf95f0d4b48d596c176dccc19876e32b3c67b77b1e72c1053cea05f6417f1c5340150037a3da1 + web-streams-polyfill: ^3.2.1 + checksum: b827ac2299676705d98d31be4b68e70741200618b38daaae0d70e587f61fae573854170e2ca219d64bbd910b96960e26b64aa9869e9194a4d162e2a751d6f7a7 + languageName: node + linkType: hard + +"@whatwg-node/node-fetch@npm:^0.3.0": + version: 0.3.0 + resolution: "@whatwg-node/node-fetch@npm:0.3.0" + dependencies: + "@whatwg-node/events": ^0.0.2 + busboy: ^1.6.0 + fast-querystring: ^1.1.1 + fast-url-parser: ^1.1.3 + tslib: ^2.3.1 + peerDependencies: + "@types/node": ^18.0.6 + checksum: 141deb3f44d1978218c64ca285120b3e2427159773056774f2251f9d5d418c8e1d6f27f4b593d031ff4124f9cd00cb4bd9fd681e4528c9f498c72356a520b69f languageName: node linkType: hard @@ -16128,13 +16658,6 @@ __metadata: languageName: node linkType: hard -"after@npm:0.8.2": - version: 0.8.2 - resolution: "after@npm:0.8.2" - checksum: 52ea9be2e97d764de256dfb3843d68cb180d2d464748010ea0d8079ee28190190080a53e4005a62adbf4337c2d613906db82f08c0091cccb8e817625ccf94c52 - languageName: node - linkType: hard - "agent-base@npm:6, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -16165,6 +16688,16 @@ __metadata: languageName: node linkType: hard +"aggregate-error@npm:^4.0.0": + version: 4.0.1 + resolution: "aggregate-error@npm:4.0.1" + dependencies: + clean-stack: ^4.0.0 + indent-string: ^5.0.0 + checksum: bb3ffdfd13447800fff237c2cba752c59868ee669104bb995dfbbe0b8320e967d679e683dabb640feb32e4882d60258165cde0baafc4cd467cc7d275a13ad6b5 + languageName: node + linkType: hard + "ajv-formats@npm:^2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" @@ -16211,7 +16744,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.8.0": +"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.12.0, ajv@npm:^8.8.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -16331,13 +16864,6 @@ __metadata: languageName: node linkType: hard -"any-base@npm:^1.1.0": - version: 1.1.0 - resolution: "any-base@npm:1.1.0" - checksum: c1fd040de52e710e2de7d9ae4df52bac589f35622adb24686c98ce21c7b824859a8db9614bc119ed8614f42fd08918b2612e6a6c385480462b3100a1af59289d - languageName: node - linkType: hard - "any-promise@npm:^1.0.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" @@ -16642,7 +17168,7 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.4, array-includes@npm:^3.1.5, array-includes@npm:^3.1.6": +"array-includes@npm:^3.1.5, array-includes@npm:^3.1.6": version: 3.1.6 resolution: "array-includes@npm:3.1.6" dependencies: @@ -16655,15 +17181,6 @@ __metadata: languageName: node linkType: hard -"array-union@npm:^1.0.1": - version: 1.0.2 - resolution: "array-union@npm:1.0.2" - dependencies: - array-uniq: ^1.0.1 - checksum: 82cec6421b6e6766556c484835a6d476a873f1b71cace5ab2b4f1b15b1e3162dc4da0d16f7a2b04d4aec18146c6638fe8f661340b31ba8e469fd811a1b45dc8d - languageName: node - linkType: hard - "array-union@npm:^2.1.0": version: 2.1.0 resolution: "array-union@npm:2.1.0" @@ -16671,22 +17188,15 @@ __metadata: languageName: node linkType: hard -"array-uniq@npm:^1.0.1": - version: 1.0.3 - resolution: "array-uniq@npm:1.0.3" - checksum: 1625f06b093d8bf279b81adfec6e72951c0857d65b5e3f65f053fffe9f9dd61c2fc52cff57e38a4700817e7e3f01a4faa433d505ea9e33cdae4514c334e0bf9e - languageName: node - linkType: hard - -"array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.2.5": - version: 1.3.0 - resolution: "array.prototype.flat@npm:1.3.0" +"array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.3.1": + version: 1.3.1 + resolution: "array.prototype.flat@npm:1.3.1" dependencies: call-bind: ^1.0.2 - define-properties: ^1.1.3 - es-abstract: ^1.19.2 + define-properties: ^1.1.4 + es-abstract: ^1.20.4 es-shim-unscopables: ^1.0.0 - checksum: 2a652b3e8dc0bebb6117e42a5ab5738af0203a14c27341d7bb2431467bdb4b348e2c5dc555dfcda8af0a5e4075c400b85311ded73861c87290a71a17c3e0a257 + checksum: 5a8415949df79bf6e01afd7e8839bbde5a3581300e8ad5d8449dea52639e9e59b26a467665622783697917b43bf39940a6e621877c7dd9b3d1c1f97484b9b88b languageName: node linkType: hard @@ -16715,13 +17225,6 @@ __metadata: languageName: node linkType: hard -"arraybuffer.slice@npm:~0.0.7": - version: 0.0.7 - resolution: "arraybuffer.slice@npm:0.0.7" - checksum: c6bacada71e8fe3e63c5f5ac5f23810e617faa1bcf33be21db43f40819edc7e186172d4a6fb8f1b9baea26689fc8da6ddeb78cefa2cc9723156c0247c3587118 - languageName: node - linkType: hard - "arrify@npm:^1.0.1": version: 1.0.1 resolution: "arrify@npm:1.0.1" @@ -16890,6 +17393,13 @@ __metadata: languageName: node linkType: hard +"avsc@npm:^5.7.3": + version: 5.7.7 + resolution: "avsc@npm:5.7.7" + checksum: e3361aa88a61397b3345876263f79c8c8bfe013d849142202758205459a37e24cdbf02edc49ae019d6e82d93bbc7bc73e9e7fefca049aae91626bae28de4d1a9 + languageName: node + linkType: hard + "aws-lambda@npm:^1.0.7": version: 1.0.7 resolution: "aws-lambda@npm:1.0.7" @@ -17021,15 +17531,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^0.25.0": - version: 0.25.0 - resolution: "axios@npm:0.25.0" - dependencies: - follow-redirects: ^1.14.7 - checksum: 2a8a3787c05f2a0c9c3878f49782357e2a9f38945b93018fb0c4fd788171c43dceefbb577988628e09fea53952744d1ecebde234b561f1e703aa43e0a598a3ad - languageName: node - linkType: hard - "axios@npm:^0.27.2": version: 0.27.2 resolution: "axios@npm:0.27.2" @@ -17276,7 +17777,7 @@ __metadata: languageName: node linkType: hard -"backo2@npm:1.0.2, backo2@npm:^1.0.2": +"backo2@npm:^1.0.2": version: 1.0.2 resolution: "backo2@npm:1.0.2" checksum: fda8d0a0f4810068d23715f2f45153146d6ee8f62dd827ce1e0b6cc3c8328e84ad61e11399a83931705cef702fe7cbb457856bf99b9bd10c4ed57b0786252385 @@ -17318,13 +17819,6 @@ __metadata: languageName: node linkType: hard -"base64-arraybuffer@npm:0.1.4": - version: 0.1.4 - resolution: "base64-arraybuffer@npm:0.1.4" - checksum: d249a929e27b2430d7ba1527e91a36e14da37ae2f80e350c4d696a038257718f8da07577e820e7262f86a0ecd573c283db10c46502080f53ae11bfdd99b6a029 - languageName: node - linkType: hard - "base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -17339,13 +17833,6 @@ __metadata: languageName: node linkType: hard -"base64id@npm:2.0.0": - version: 2.0.0 - resolution: "base64id@npm:2.0.0" - checksum: 581b1d37e6cf3738b7ccdd4d14fe2bfc5c238e696e2720ee6c44c183b838655842e22034e53ffd783f872a539915c51b0d4728a49c7cc678ac5a758e00d62168 - languageName: node - linkType: hard - "base64url@npm:3.x.x": version: 3.0.1 resolution: "base64url@npm:3.0.1" @@ -17395,13 +17882,13 @@ __metadata: linkType: hard "better-sqlite3@npm:^8.0.0": - version: 8.0.1 - resolution: "better-sqlite3@npm:8.0.1" + version: 8.1.0 + resolution: "better-sqlite3@npm:8.1.0" dependencies: bindings: ^1.5.0 node-gyp: latest prebuild-install: ^7.1.0 - checksum: 533b2cc32bd54e2a943a4f63e079f8cc8945879b7a0ebd2085b948824ea067792f6f903ab75ba75d7cafc5f8f973354cdf9a042fa6216e077925d07a8df1bead + checksum: 7e6b4c33142f4b2ee2e60d3926b4db682feb0d09554efdaf7bea53cd5bef406ca0a5115ab9b411458daa45ba14973252ebd6853a3cdf94e4fd49cc8291730ade languageName: node linkType: hard @@ -17452,10 +17939,10 @@ __metadata: languageName: node linkType: hard -"binary-extensions@npm:^2.0.0": - version: 2.0.0 - resolution: "binary-extensions@npm:2.0.0" - checksum: 554f65d3378cf71c3185c17dec3ca58334b8ff6ae242db3107284765ce33b2af19efd20c11faec41907a40534929e34b3a98e7d391c61e4211b45732dccb1115 +"binary-extensions@npm:^2.0.0, binary-extensions@npm:^2.2.0": + version: 2.2.0 + resolution: "binary-extensions@npm:2.2.0" + checksum: ccd267956c58d2315f5d3ea6757cf09863c5fc703e50fbeb13a7dc849b812ef76e3cf9ca8f35a0c48498776a7478d7b4a0418e1e2b8cb9cb9731f2922aaad7f8 languageName: node linkType: hard @@ -17507,13 +17994,6 @@ __metadata: languageName: node linkType: hard -"blob@npm:0.0.5": - version: 0.0.5 - resolution: "blob@npm:0.0.5" - checksum: ca6a025f1108b7fd317fba0a0d64424f5cf47be6f4653ec8ba44777804a4e5c86c8d7a7e41f75fe52fd77c5bfc3479f68e017abe649a241c15c3fc07bfc59e7b - languageName: node - linkType: hard - "bluebird@npm:3.7.2, bluebird@npm:^3.3.5, bluebird@npm:^3.5.5, bluebird@npm:^3.7.2": version: 3.7.2 resolution: "bluebird@npm:3.7.2" @@ -17521,13 +18001,6 @@ __metadata: languageName: node linkType: hard -"bmp-js@npm:^0.1.0": - version: 0.1.0 - resolution: "bmp-js@npm:0.1.0" - checksum: 2f6cf7eeabae2aa50eb768122f59e9752caa97248028cb8b5cf0d9db7cf8fb3a60262aeb2c6889dd21357ab061b2fb318f21f20d2b24963ba36ead8e264c6654 - languageName: node - linkType: hard - "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.1.1, bn.js@npm:^4.11.9": version: 4.12.0 resolution: "bn.js@npm:4.12.0" @@ -17623,6 +18096,13 @@ __metadata: languageName: node linkType: hard +"brcast@npm:^3.0.1": + version: 3.0.2 + resolution: "brcast@npm:3.0.2" + checksum: 7abae42088c6ffad9ff9e0fc756607a1764e299d737b3007fa49a73a38b3fda1e51362713f645db7991878ca388de94942011188450324c3debd08509315fa94 + languageName: node + linkType: hard + "breakword@npm:^1.0.5": version: 1.0.5 resolution: "breakword@npm:1.0.5" @@ -17768,13 +18248,6 @@ __metadata: languageName: node linkType: hard -"buffer-equal@npm:0.0.1": - version: 0.0.1 - resolution: "buffer-equal@npm:0.0.1" - checksum: ca4b52e6c01143529d957a78cb9a93e4257f172bbab30d9eb87c20ae085ed23c5e07f236ac051202dacbf3d17aba42e1455f84cba21ea79b67d57f2b05e9a613 - languageName: node - linkType: hard - "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -17817,7 +18290,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.2.0, buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.6.0": +"buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.6.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -18205,6 +18678,13 @@ __metadata: languageName: node linkType: hard +"change-emitter@npm:^0.1.2": + version: 0.1.6 + resolution: "change-emitter@npm:0.1.6" + checksum: 0ed494ba9901ca56ea6f942668fd294465c334a9a0981dca96da5aea5e387c0023a630d7c658c1b532d203db54c928ddca2564e434b4a8b7f6d39155d09db255 + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -18396,6 +18876,15 @@ __metadata: languageName: node linkType: hard +"clean-stack@npm:^4.0.0": + version: 4.2.0 + resolution: "clean-stack@npm:4.2.0" + dependencies: + escape-string-regexp: 5.0.0 + checksum: 373f656a31face5c615c0839213b9b542a0a48057abfb1df66900eab4dc2a5c6097628e4a0b5aa559cdfc4e66f8a14ea47be9681773165a44470ef5fb8ccc172 + languageName: node + linkType: hard + "cli-cursor@npm:^3.1.0": version: 3.1.0 resolution: "cli-cursor@npm:3.1.0" @@ -18461,6 +18950,17 @@ __metadata: languageName: node linkType: hard +"clipboard@npm:^2.0.4": + version: 2.0.11 + resolution: "clipboard@npm:2.0.11" + dependencies: + good-listener: ^1.2.2 + select: ^1.1.2 + tiny-emitter: ^2.0.0 + checksum: 413055a6038e43898e0e895216b58ed54fbf386f091cb00188875ef35b186cefbd258acdf4cb4b0ac87cbc00de936f41b45dde9fe1fd1a57f7babb28363b8748 + languageName: node + linkType: hard + "cliui@npm:^3.2.0": version: 3.2.0 resolution: "cliui@npm:3.2.0" @@ -18726,7 +19226,7 @@ __metadata: languageName: node linkType: hard -"color@npm:^4.0.1": +"color@npm:^4.0.1, color@npm:^4.2.3": version: 4.2.3 resolution: "color@npm:4.2.3" dependencies: @@ -18811,7 +19311,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:*, commander@npm:^9.1.0, commander@npm:^9.4.1": +"commander@npm:*, commander@npm:^9.1.0, commander@npm:^9.4.1, commander@npm:^9.5.0": version: 9.5.0 resolution: "commander@npm:9.5.0" checksum: c7a3e27aa59e913b54a1bafd366b88650bc41d6651f0cbe258d4ff09d43d6a7394232a4dadd0bf518b3e696fdf595db1028a0d82c785b88bd61f8a440cecfade @@ -18895,6 +19395,20 @@ __metadata: languageName: node linkType: hard +"commonmark@npm:^0.29.0": + version: 0.29.3 + resolution: "commonmark@npm:0.29.3" + dependencies: + entities: ~2.0 + mdurl: ~1.0.1 + minimist: ">=1.2.2" + string.prototype.repeat: ^0.2.0 + bin: + commonmark: bin/commonmark + checksum: 892b603aeffea6a04770ce24979fc4024d2d92ca6d5146353398faab15be980af6802f3d7b35357400b870e752a9f80b766fed5c09ca063a2951c8c26013876c + languageName: node + linkType: hard + "compare-versions@npm:4.1.3": version: 4.1.3 resolution: "compare-versions@npm:4.1.3" @@ -18902,34 +19416,13 @@ __metadata: languageName: node linkType: hard -"component-bind@npm:1.0.0": - version: 1.0.0 - resolution: "component-bind@npm:1.0.0" - checksum: 746c5810b9f8735643840ad04072e1ab817444d44dc1aadc813f1f1a17c47c27616584caa0db93db7e687bfe73b65073d8246c785bcdac80f8f3627d3bb26883 - languageName: node - linkType: hard - -"component-emitter@npm:1.2.1": - version: 1.2.1 - resolution: "component-emitter@npm:1.2.1" - checksum: 00599b827635cab65bb20e5e3e2db4cea120b76b6626ce3ac6c85d7f5f39bbadd9fec530da444380035dd1c8ff08f9badca54d40b68feaf74bc64f75d537ef61 - languageName: node - linkType: hard - -"component-emitter@npm:^1.3.0, component-emitter@npm:~1.3.0": +"component-emitter@npm:^1.3.0": version: 1.3.0 resolution: "component-emitter@npm:1.3.0" checksum: b3c46de38ffd35c57d1c02488355be9f218e582aec72d72d1b8bbec95a3ac1b38c96cd6e03ff015577e68f550fbb361a3bfdbd9bb248be9390b7b3745691be6b languageName: node linkType: hard -"component-inherit@npm:0.0.3": - version: 0.0.3 - resolution: "component-inherit@npm:0.0.3" - checksum: 9f5b872a6b3a396cf827d471d07db5626d7918202ab6c1d59f2b849bab18ce4fe61d9b2b7b6ae5cd547b81f27a3de5ae678f438bfe4fadce8f26a1526ba86a74 - languageName: node - linkType: hard - "compress-commons@npm:^4.1.0": version: 4.1.0 resolution: "compress-commons@npm:4.1.0" @@ -19173,7 +19666,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.4.2, cookie@npm:^0.4.2, cookie@npm:~0.4.1": +"cookie@npm:0.4.2, cookie@npm:^0.4.2": version: 0.4.2 resolution: "cookie@npm:0.4.2" checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b @@ -19188,9 +19681,9 @@ __metadata: linkType: hard "cookiejar@npm:^2.1.3": - version: 2.1.3 - resolution: "cookiejar@npm:2.1.3" - checksum: 88259983ebc52ceb23cdacfa48762b6a518a57872eff1c7ed01d214fff5cf492e2660d7d5c04700a28f1787a76811df39e8639f8e17670b3cf94ecd86e161f07 + version: 2.1.4 + resolution: "cookiejar@npm:2.1.4" + checksum: c4442111963077dc0e5672359956d6556a195d31cbb35b528356ce5f184922b99ac48245ac05ed86cf993f7df157c56da10ab3efdadfed79778a0d9b1b092d5b languageName: node linkType: hard @@ -19220,6 +19713,13 @@ __metadata: languageName: node linkType: hard +"core-js@npm:^1.0.0": + version: 1.2.7 + resolution: "core-js@npm:1.2.7" + checksum: 0b76371bfa98708351cde580f9287e2360d2209920e738ae950ae74ad08639a2e063541020bf666c28778956fc356ed9fe56d962129c88a87a6a4a0612526c75 + languageName: node + linkType: hard + "core-js@npm:^2.4.0, core-js@npm:^2.5.0": version: 2.6.12 resolution: "core-js@npm:2.6.12" @@ -19227,10 +19727,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.4.1, core-js@npm:^3.6.5": - version: 3.27.1 - resolution: "core-js@npm:3.27.1" - checksum: d50b5f88aea4302512ad9446c18e90f4d35dea1e6d8d3f87337690677061565ff11a670f1e0c87de57aa6074375fbb25ed5784076c040d3c4de8b4bce7d2ebeb +"core-js@npm:^3.6.5": + version: 3.28.0 + resolution: "core-js@npm:3.28.0" + checksum: 3155fd0ec16d0089106b145e9595280a4ea4bde0d7ff26aa14364cd4f1c203baf6620c3025acd284f363d08b9f21104101692766ca9a36ffeee7307bdf3e1881 languageName: node linkType: hard @@ -19251,7 +19751,7 @@ __metadata: languageName: node linkType: hard -"cosmiconfig-typescript-loader@npm:4.3.0": +"cosmiconfig-typescript-loader@npm:^4.3.0": version: 4.3.0 resolution: "cosmiconfig-typescript-loader@npm:4.3.0" peerDependencies: @@ -19394,11 +19894,11 @@ __metadata: linkType: hard "cronstrue@npm:^2.2.0": - version: 2.21.0 - resolution: "cronstrue@npm:2.21.0" + version: 2.23.0 + resolution: "cronstrue@npm:2.23.0" bin: cronstrue: bin/cli.js - checksum: c77206339b22c30f0d0c584dad19614cd29d712a00afc3cd5d6ebab8b5e651883e72c04832b82b55c4dcf4f6a9daff6c6c7023364770f7dff82ada29616d40a5 + checksum: 050140ac6452468d6f9032f18038f41b16968502d411a996efd5d42243cfd882271ff20ed4a3cdb25c45bf8f88beeb0579e45e974a11987401e9d383af2641a8 languageName: node linkType: hard @@ -19584,6 +20084,15 @@ __metadata: languageName: node linkType: hard +"css-vendor@npm:^0.3.8": + version: 0.3.8 + resolution: "css-vendor@npm:0.3.8" + dependencies: + is-in-browser: ^1.0.2 + checksum: 0a2e0cd0d4adbfdb6236950e7f9697b8a9b294eb2ae7c95996a95d273d2a63316ce793cb4654ae048aa3c129327124d2a29aada9935a0c284f9cc341c2768c8a + languageName: node + linkType: hard + "css-vendor@npm:^2.0.8": version: 2.0.8 resolution: "css-vendor@npm:2.0.8" @@ -19725,10 +20234,10 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^2.5.2, csstype@npm:^2.6.7": - version: 2.6.17 - resolution: "csstype@npm:2.6.17" - checksum: 6eee5cf81a4b1b2f0e8707b4accd7504f7cceb4b5a496d58c6e4fcea1a70c1443a975e45d722c892d372ffe788fa278ddfe4d70c5f881375f34e48bb99c70ecc +"csstype@npm:^2.0.0, csstype@npm:^2.5.2, csstype@npm:^2.6.7": + version: 2.6.21 + resolution: "csstype@npm:2.6.21" + checksum: 2ce8bc832375146eccdf6115a1f8565a27015b74cce197c35103b4494955e9516b246140425ad24103864076aa3e1257ac9bab25a06c8d931dd87a6428c9dccf languageName: node linkType: hard @@ -19772,31 +20281,6 @@ __metadata: languageName: node linkType: hard -"cypress-plugin-snapshots@npm:^1.4.4": - version: 1.4.4 - resolution: "cypress-plugin-snapshots@npm:1.4.4" - dependencies: - diff2html: ^2.7.0 - fs-extra: ^7.0.1 - image-size: ^0.7.2 - jimp: ^0.10.3 - js-base64: ^2.5.1 - lodash: ^4.17.13 - pixelmatch: ^4.0.2 - pngjs: ^3.3.3 - prettier: ^1.16.4 - rimraf: ^2.6.3 - sanitize-filename: ^1.6.1 - socket.io: ^2.2.0 - socket.io-client: ^2.2.0 - source-map-support: ^0.5.10 - unidiff: 1.0.2 - peerDependencies: - cypress: ^4.5.0 - checksum: 75b8d6b6e0353f7d2ac617c503f33c84c7c5f4e650dbaf59a76558e971bcc82532d2ac37b9eeccb7382354a13fe5d0068c8e97a9f7f6f1af6135cf5286c285f9 - languageName: node - linkType: hard - "cypress@npm:^10.0.0": version: 10.11.0 resolution: "cypress@npm:10.11.0" @@ -20092,13 +20576,20 @@ __metadata: languageName: node linkType: hard -"dataloader@npm:2.1.0, dataloader@npm:^2.0.0": +"dataloader@npm:2.1.0": version: 2.1.0 resolution: "dataloader@npm:2.1.0" checksum: bbd43496c41697766f8611f60ed3bae437b450985499140b9603a2447c10d45975d3a0661caaebaa9750bc1bc9fd154676a314423294916c8b8ca1a59a08d139 languageName: node linkType: hard +"dataloader@npm:^2.0.0": + version: 2.2.2 + resolution: "dataloader@npm:2.2.2" + checksum: 4dabd247089c29f194e94d5434d504f99156c5c214a03463c20f3f17f40398d7e179edee69a27c16e315519ac8739042a810090087ae26449a0e685156a02c65 + languageName: node + linkType: hard + "date-fns@npm:^2.16.1, date-fns@npm:^2.18.0, date-fns@npm:^2.29.1": version: 2.29.3 resolution: "date-fns@npm:2.29.3" @@ -20127,14 +20618,14 @@ __metadata: languageName: node linkType: hard -"debounce@npm:^1.2.0": - version: 1.2.0 - resolution: "debounce@npm:1.2.0" - checksum: e39cb593ae26344921f5a2681b40b703bdd22bc43b179f0e7515176c790997932b3a0ee6ea9864f384c6ac58cecc08158fb102c3632d5d88ab621f8230ee39ff +"debounce@npm:^1.1.0, debounce@npm:^1.2.0": + version: 1.2.1 + resolution: "debounce@npm:1.2.1" + checksum: 682a89506d9e54fb109526f4da255c5546102fbb8e3ae75eef3b04effaf5d4853756aee97475cd4650641869794e44f410eeb20ace2b18ea592287ab2038519e languageName: node linkType: hard -"debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.9": +"debug@npm:2.6.9, debug@npm:^2.6.0": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -20164,24 +20655,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:~3.1.0": - version: 3.1.0 - resolution: "debug@npm:3.1.0" - dependencies: - ms: 2.0.0 - checksum: 0b52718ab957254a5b3ca07fc34543bc778f358620c206a08452251eb7fc193c3ea3505072acbf4350219c14e2d71ceb7bdaa0d3370aa630b50da790458d08b3 - languageName: node - linkType: hard - -"debug@npm:~4.1.0": - version: 4.1.1 - resolution: "debug@npm:4.1.1" - dependencies: - ms: ^2.1.1 - checksum: 1e681f5cce94ba10f8dde74b20b42e4d8cf0d2a6700f4c165bb3bb6885565ef5ca5885bf07e704974a835f2415ff095a63164f539988a1f07e8a69fe8b1d65ad - languageName: node - linkType: hard - "debuglog@npm:^1.0.1": version: 1.0.1 resolution: "debuglog@npm:1.0.1" @@ -20300,6 +20773,13 @@ __metadata: languageName: node linkType: hard +"deepmerge@npm:^3.0.0": + version: 3.3.0 + resolution: "deepmerge@npm:3.3.0" + checksum: 4322195389e0170a0443c07b36add19b90249802c4b47b96265fdc5f5d8beddf491d5e50cbc5bfd65f85ccf76598173083863c202f5463b3b667aca8be75d5ac + languageName: node + linkType: hard + "deepmerge@npm:^4.2.2, deepmerge@npm:~4.2.2": version: 4.2.2 resolution: "deepmerge@npm:4.2.2" @@ -20349,19 +20829,19 @@ __metadata: languageName: node linkType: hard -"del@npm:^6.0.0": - version: 6.1.1 - resolution: "del@npm:6.1.1" +"del@npm:^7.0.0": + version: 7.0.0 + resolution: "del@npm:7.0.0" dependencies: - globby: ^11.0.1 - graceful-fs: ^4.2.4 - is-glob: ^4.0.1 - is-path-cwd: ^2.2.0 - is-path-inside: ^3.0.2 - p-map: ^4.0.0 + globby: ^13.1.2 + graceful-fs: ^4.2.10 + is-glob: ^4.0.3 + is-path-cwd: ^3.0.0 + is-path-inside: ^4.0.0 + p-map: ^5.5.0 rimraf: ^3.0.2 - slash: ^3.0.0 - checksum: 563288b73b8b19a7261c47fd21a330eeab6e2acd7c6208c49790dfd369127120dd7836cdf0c1eca216b77c94782a81507eac6b4734252d3bef2795cb366996b6 + slash: ^4.0.0 + checksum: 33e5077f18b5dfbe81971d1f8a2cd8bf676dd5ede491bab85ec17a4a1d59001bd3ec47fd38e9a4ae01a3c98c07b98c7b3dc56190b86d88926798802d7858d827 languageName: node linkType: hard @@ -20379,6 +20859,13 @@ __metadata: languageName: node linkType: hard +"delegate@npm:^3.1.2": + version: 3.2.0 + resolution: "delegate@npm:3.2.0" + checksum: d943058fe05897228b158cbd1bab05164df28c8f54127873231d6b03b0a5acc1b3ee1f98ac70ccc9b79cd84aa47118a7de111fee2923753491583905069da27d + languageName: node + linkType: hard + "delegates@npm:^1.0.0": version: 1.0.0 resolution: "delegates@npm:1.0.0" @@ -20386,10 +20873,10 @@ __metadata: languageName: node linkType: hard -"denque@npm:^2.0.1": - version: 2.0.1 - resolution: "denque@npm:2.0.1" - checksum: ec398d1e3c6c8d4f5213dcf9ad74d7faa3b461e29a0019c9742b49a97ac5e16aa7134db45fa9d841e318e7722dd1ba670a474fde9a5b0d870b3a5fc6fe914c30 +"denque@npm:^2.0.1, denque@npm:^2.1.0": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 languageName: node linkType: hard @@ -20461,7 +20948,7 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.0": +"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1": version: 2.0.1 resolution: "detect-libc@npm:2.0.1" checksum: ccb05fcabbb555beb544d48080179c18523a343face9ee4e1a86605a8715b4169f94d663c21a03c310ac824592f2ba9a5270218819bb411ad7be578a527593d7 @@ -20526,18 +21013,6 @@ __metadata: languageName: node linkType: hard -"diff2html@npm:^2.7.0": - version: 2.12.2 - resolution: "diff2html@npm:2.12.2" - dependencies: - diff: ^4.0.1 - hogan.js: ^3.0.2 - merge: ^1.2.1 - whatwg-fetch: ^3.0.0 - checksum: 4d659fb558575406881bc9e6b892a236d038bca1335cbdde65ebfde462cf1cff7a7ab4a2e82d9874205e658dd2caa31699ba24ea4efb714d7c7277ddb96c7768 - languageName: node - linkType: hard - "diff3@npm:0.0.3": version: 0.0.3 resolution: "diff3@npm:0.0.3" @@ -20545,13 +21020,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^2.2.2": - version: 2.2.3 - resolution: "diff@npm:2.2.3" - checksum: b5b6fbe1848c621ef6f6c154e689842f7bb992b4c0ae257b70f5577b0906812537bf2eccda14d0215f90579567c8880eb44f1cf3851bd615583214a4f4d61ce3 - languageName: node - linkType: hard - "diff@npm:^4.0.1": version: 4.0.2 resolution: "diff@npm:4.0.2" @@ -20577,15 +21045,6 @@ __metadata: languageName: node linkType: hard -"dir-glob@npm:^2.0.0": - version: 2.2.2 - resolution: "dir-glob@npm:2.2.2" - dependencies: - path-type: ^3.0.0 - checksum: 3aa48714a9f7845ffc30ab03a5c674fe760477cc55e67b0847333371549227d93953e6627ec160f75140c5bea5c5f88d13c01de79bd1997a588efbcf06980842 - languageName: node - linkType: hard - "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -20677,7 +21136,7 @@ __metadata: languageName: node linkType: hard -"dom-helpers@npm:^3.4.0": +"dom-helpers@npm:^3.2.1, dom-helpers@npm:^3.4.0": version: 3.4.0 resolution: "dom-helpers@npm:3.4.0" dependencies: @@ -20718,13 +21177,6 @@ __metadata: languageName: node linkType: hard -"dom-walk@npm:^0.1.0": - version: 0.1.1 - resolution: "dom-walk@npm:0.1.1" - checksum: f55db10d0344b87cca1046a2356915a8e9ddee90fc97d3ebb26c4021e593cce47d24cbef18aac721421787c6a60fa14dd079a0b81f8610c9aeeec52177fd2acf - languageName: node - linkType: hard - "domain-browser@npm:^1.1.1": version: 1.2.0 resolution: "domain-browser@npm:1.2.0" @@ -20867,6 +21319,7 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" + "@backstage/create-app": "workspace:^" "@backstage/errors": "workspace:^" "@types/fs-extra": ^9.0.1 "@types/node": ^16.11.26 @@ -21031,7 +21484,7 @@ __metadata: languageName: node linkType: hard -"encoding@npm:^0.1.12, encoding@npm:^0.1.13": +"encoding@npm:^0.1.11, encoding@npm:^0.1.12, encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" dependencies: @@ -21049,52 +21502,6 @@ __metadata: languageName: node linkType: hard -"engine.io-client@npm:~3.5.0": - version: 3.5.2 - resolution: "engine.io-client@npm:3.5.2" - dependencies: - component-emitter: ~1.3.0 - component-inherit: 0.0.3 - debug: ~3.1.0 - engine.io-parser: ~2.2.0 - has-cors: 1.1.0 - indexof: 0.0.1 - parseqs: 0.0.6 - parseuri: 0.0.6 - ws: ~7.4.2 - xmlhttprequest-ssl: ~1.6.2 - yeast: 0.1.2 - checksum: 2a4a8407ea939c45826d3483e39fe017e66d488dfd101282a79644f75196f7b84cac9d6f981e3ace1ab231e2469c18b25d79b114f20ab58d0eaa393bf66f213e - languageName: node - linkType: hard - -"engine.io-parser@npm:~2.2.0": - version: 2.2.1 - resolution: "engine.io-parser@npm:2.2.1" - dependencies: - after: 0.8.2 - arraybuffer.slice: ~0.0.7 - base64-arraybuffer: 0.1.4 - blob: 0.0.5 - has-binary2: ~1.0.2 - checksum: c7291955c1af4b6c384c2642c2d10a4df0ca7b6acc5cb178dd4933bcb1522eccc3d89ffae6e6080ce760451378fce49db47af3fb9999cae08de310e75f3c29d6 - languageName: node - linkType: hard - -"engine.io@npm:~3.6.0": - version: 3.6.1 - resolution: "engine.io@npm:3.6.1" - dependencies: - accepts: ~1.3.4 - base64id: 2.0.0 - cookie: ~0.4.1 - debug: ~4.1.0 - engine.io-parser: ~2.2.0 - ws: ~7.4.2 - checksum: 7dd9eddc79aedf61a63141e6208ee89936c659445378f965bff6d5eb3e9cb4815aff735f2b1886487e2be51116a343e89d972ae5790bf2ac9fdcb2572baf8935 - languageName: node - linkType: hard - "enhanced-resolve@npm:^5.10.0": version: 5.10.0 resolution: "enhanced-resolve@npm:5.10.0" @@ -21135,6 +21542,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:~2.0": + version: 2.0.3 + resolution: "entities@npm:2.0.3" + checksum: 5a7899fcc622e0d76afdeafe4c58a6b40ae3a8ee4772e5825a648c11a2ca324a9a02515386f512e466baac4aeb551f3d3b79eaece5cd98369b9f8601be336b1a + languageName: node + linkType: hard + "entities@npm:~2.1.0": version: 2.1.0 resolution: "entities@npm:2.1.0" @@ -21188,7 +21602,7 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.2, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.4": +"es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.4": version: 1.20.4 resolution: "es-abstract@npm:1.20.4" dependencies: @@ -21369,18 +21783,18 @@ __metadata: linkType: hard "esbuild-loader@npm:^2.18.0": - version: 2.20.0 - resolution: "esbuild-loader@npm:2.20.0" + version: 2.21.0 + resolution: "esbuild-loader@npm:2.21.0" dependencies: - esbuild: ^0.15.6 + esbuild: ^0.16.17 joycon: ^3.0.1 json5: ^2.2.0 loader-utils: ^2.0.0 tapable: ^2.2.0 - webpack-sources: ^2.2.0 + webpack-sources: ^1.4.3 peerDependencies: webpack: ^4.40.0 || ^5.0.0 - checksum: 81faee7155b35af1fdef3dffa273a14ec83e56b9efa1efb76cb1eb64964dd738809c147a87ab9d3507de11946eed51fd1ee42d476b2c9654cbda145da0d9479b + checksum: a0456ed7794e2c220a6068e92d739bc19765bff352bf7e44442aa8127631cc517ecd02a3ee969e31fa6b6a91befeac928296488c95e3818a776cd3b11d46348c languageName: node linkType: hard @@ -21426,7 +21840,161 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.15.6": +"esbuild@npm:^0.16.17": + version: 0.16.17 + resolution: "esbuild@npm:0.16.17" + dependencies: + "@esbuild/android-arm": 0.16.17 + "@esbuild/android-arm64": 0.16.17 + "@esbuild/android-x64": 0.16.17 + "@esbuild/darwin-arm64": 0.16.17 + "@esbuild/darwin-x64": 0.16.17 + "@esbuild/freebsd-arm64": 0.16.17 + "@esbuild/freebsd-x64": 0.16.17 + "@esbuild/linux-arm": 0.16.17 + "@esbuild/linux-arm64": 0.16.17 + "@esbuild/linux-ia32": 0.16.17 + "@esbuild/linux-loong64": 0.16.17 + "@esbuild/linux-mips64el": 0.16.17 + "@esbuild/linux-ppc64": 0.16.17 + "@esbuild/linux-riscv64": 0.16.17 + "@esbuild/linux-s390x": 0.16.17 + "@esbuild/linux-x64": 0.16.17 + "@esbuild/netbsd-x64": 0.16.17 + "@esbuild/openbsd-x64": 0.16.17 + "@esbuild/sunos-x64": 0.16.17 + "@esbuild/win32-arm64": 0.16.17 + "@esbuild/win32-ia32": 0.16.17 + "@esbuild/win32-x64": 0.16.17 + dependenciesMeta: + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 4c2cc609ecfb426554bc3f75beb92d89eb2d0c515cfceebaa36c7599d7dcaab7056b70f6d6b51e72b45951ddf9021ee28e356cf205f8e42cc055d522312ea30c + languageName: node + linkType: hard + +"esbuild@npm:^0.17.0": + version: 0.17.10 + resolution: "esbuild@npm:0.17.10" + dependencies: + "@esbuild/android-arm": 0.17.10 + "@esbuild/android-arm64": 0.17.10 + "@esbuild/android-x64": 0.17.10 + "@esbuild/darwin-arm64": 0.17.10 + "@esbuild/darwin-x64": 0.17.10 + "@esbuild/freebsd-arm64": 0.17.10 + "@esbuild/freebsd-x64": 0.17.10 + "@esbuild/linux-arm": 0.17.10 + "@esbuild/linux-arm64": 0.17.10 + "@esbuild/linux-ia32": 0.17.10 + "@esbuild/linux-loong64": 0.17.10 + "@esbuild/linux-mips64el": 0.17.10 + "@esbuild/linux-ppc64": 0.17.10 + "@esbuild/linux-riscv64": 0.17.10 + "@esbuild/linux-s390x": 0.17.10 + "@esbuild/linux-x64": 0.17.10 + "@esbuild/netbsd-x64": 0.17.10 + "@esbuild/openbsd-x64": 0.17.10 + "@esbuild/sunos-x64": 0.17.10 + "@esbuild/win32-arm64": 0.17.10 + "@esbuild/win32-ia32": 0.17.10 + "@esbuild/win32-x64": 0.17.10 + dependenciesMeta: + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 803de327036528c140b3d1d8e148604fd1446062b63d2b5a49cd8fe5fa607dc41be915f28dec1242be77164378e3ca27a2ed2968692a73cc833896c7bebc0e12 + languageName: node + linkType: hard + +"esbuild@npm:~0.15.10": version: 0.15.18 resolution: "esbuild@npm:0.15.18" dependencies: @@ -21503,83 +22071,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.16.0": - version: 0.16.16 - resolution: "esbuild@npm:0.16.16" - dependencies: - "@esbuild/android-arm": 0.16.16 - "@esbuild/android-arm64": 0.16.16 - "@esbuild/android-x64": 0.16.16 - "@esbuild/darwin-arm64": 0.16.16 - "@esbuild/darwin-x64": 0.16.16 - "@esbuild/freebsd-arm64": 0.16.16 - "@esbuild/freebsd-x64": 0.16.16 - "@esbuild/linux-arm": 0.16.16 - "@esbuild/linux-arm64": 0.16.16 - "@esbuild/linux-ia32": 0.16.16 - "@esbuild/linux-loong64": 0.16.16 - "@esbuild/linux-mips64el": 0.16.16 - "@esbuild/linux-ppc64": 0.16.16 - "@esbuild/linux-riscv64": 0.16.16 - "@esbuild/linux-s390x": 0.16.16 - "@esbuild/linux-x64": 0.16.16 - "@esbuild/netbsd-x64": 0.16.16 - "@esbuild/openbsd-x64": 0.16.16 - "@esbuild/sunos-x64": 0.16.16 - "@esbuild/win32-arm64": 0.16.16 - "@esbuild/win32-ia32": 0.16.16 - "@esbuild/win32-x64": 0.16.16 - dependenciesMeta: - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: d3163ec01e017776df6b68e1825caa2323918f0d03eb92250bdcdff80410a2c0eb5b3807955db84d83b1b91cf24af9815a1d19efc2343c490be3e5d7b27a834f - languageName: node - linkType: hard - "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -21594,6 +22085,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:5.0.0, escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e + languageName: node + linkType: hard + "escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -21615,13 +22113,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^5.0.0": - version: 5.0.0 - resolution: "escape-string-regexp@npm:5.0.0" - checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e - languageName: node - linkType: hard - "escodegen@npm:^1.13.0": version: 1.14.3 resolution: "escodegen@npm:1.14.3" @@ -21684,23 +22175,26 @@ __metadata: languageName: node linkType: hard -"eslint-import-resolver-node@npm:^0.3.6": - version: 0.3.6 - resolution: "eslint-import-resolver-node@npm:0.3.6" +"eslint-import-resolver-node@npm:^0.3.7": + version: 0.3.7 + resolution: "eslint-import-resolver-node@npm:0.3.7" dependencies: debug: ^3.2.7 - resolve: ^1.20.0 - checksum: 6266733af1e112970e855a5bcc2d2058fb5ae16ad2a6d400705a86b29552b36131ffc5581b744c23d550de844206fb55e9193691619ee4dbf225c4bde526b1c8 + is-core-module: ^2.11.0 + resolve: ^1.22.1 + checksum: 3379aacf1d2c6952c1b9666c6fa5982c3023df695430b0d391c0029f6403a7775414873d90f397e98ba6245372b6c8960e16e74d9e4a3b0c0a4582f3bdbe3d6e languageName: node linkType: hard -"eslint-module-utils@npm:^2.1.1, eslint-module-utils@npm:^2.7.3": - version: 2.7.3 - resolution: "eslint-module-utils@npm:2.7.3" +"eslint-module-utils@npm:^2.7.4": + version: 2.7.4 + resolution: "eslint-module-utils@npm:2.7.4" dependencies: debug: ^3.2.7 - find-up: ^2.1.0 - checksum: 77048263f309167a1e6a1e1b896bfb5ddd1d3859b2e2abbd9c32c432aee13d610d46e6820b1ca81b37fba437cf423a404bc6649be64ace9148a3062d1886a678 + peerDependenciesMeta: + eslint: + optional: true + checksum: 5da13645daff145a5c922896b258f8bba560722c3767254e458d894ff5fbb505d6dfd945bffa932a5b0ae06714da2379bd41011c4c20d2d59cc83e23895360f7 languageName: node linkType: hard @@ -21730,25 +22224,27 @@ __metadata: linkType: hard "eslint-plugin-import@npm:^2.25.4": - version: 2.26.0 - resolution: "eslint-plugin-import@npm:2.26.0" + version: 2.27.5 + resolution: "eslint-plugin-import@npm:2.27.5" dependencies: - array-includes: ^3.1.4 - array.prototype.flat: ^1.2.5 - debug: ^2.6.9 + array-includes: ^3.1.6 + array.prototype.flat: ^1.3.1 + array.prototype.flatmap: ^1.3.1 + debug: ^3.2.7 doctrine: ^2.1.0 - eslint-import-resolver-node: ^0.3.6 - eslint-module-utils: ^2.7.3 + eslint-import-resolver-node: ^0.3.7 + eslint-module-utils: ^2.7.4 has: ^1.0.3 - is-core-module: ^2.8.1 + is-core-module: ^2.11.0 is-glob: ^4.0.3 minimatch: ^3.1.2 - object.values: ^1.1.5 - resolve: ^1.22.0 + object.values: ^1.1.6 + resolve: ^1.22.1 + semver: ^6.3.0 tsconfig-paths: ^3.14.1 peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - checksum: 0bf77ad80339554481eafa2b1967449e1f816b94c7a6f9614ce33fb4083c4e6c050f10d241dd50b4975d47922880a34de1e42ea9d8e6fd663ebb768baa67e655 + checksum: f500571a380167e25d72a4d925ef9a7aae8899eada57653e5f3051ec3d3c16d08271fcefe41a30a9a2f4fefc232f066253673ee4ea77b30dba65ae173dade85d languageName: node linkType: hard @@ -21770,8 +22266,8 @@ __metadata: linkType: hard "eslint-plugin-jsx-a11y@npm:^6.5.1": - version: 6.7.0 - resolution: "eslint-plugin-jsx-a11y@npm:6.7.0" + version: 6.7.1 + resolution: "eslint-plugin-jsx-a11y@npm:6.7.1" dependencies: "@babel/runtime": ^7.20.7 aria-query: ^5.1.3 @@ -21791,22 +22287,7 @@ __metadata: semver: ^6.3.0 peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - checksum: b7ea212bcf84912d264229e5e3cf255bc95a1193de1c7453d275a7afc959ce679c1bffb77cfd3d17f9b7105f41e0f62c8edb7f6d76985c79647edaa9f08aa814 - languageName: node - linkType: hard - -"eslint-plugin-monorepo@npm:^0.3.2": - version: 0.3.2 - resolution: "eslint-plugin-monorepo@npm:0.3.2" - dependencies: - eslint-module-utils: ^2.1.1 - get-monorepo-packages: ^1.1.0 - globby: ^7.1.1 - load-json-file: ^4.0.0 - minimatch: ^3.0.4 - parse-package-name: ^0.1.0 - path-is-inside: ^1.0.2 - checksum: b6f17efbc9e66aefbf7aacf9aa130bc8e729a6311c412d1b0026ce7d93361c10b0e6e4b10cf434f24ca9a8b2e891bc8e5b82e66dc9ccc5e782f9c18a989eca28 + checksum: f166dd5fe7257c7b891c6692e6a3ede6f237a14043ae3d97581daf318fc5833ddc6b4871aa34ab7656187430170500f6d806895747ea17ecdf8231a666c3c2fd languageName: node linkType: hard @@ -21833,8 +22314,8 @@ __metadata: linkType: hard "eslint-plugin-react@npm:^7.28.0": - version: 7.32.1 - resolution: "eslint-plugin-react@npm:7.32.1" + version: 7.32.2 + resolution: "eslint-plugin-react@npm:7.32.2" dependencies: array-includes: ^3.1.6 array.prototype.flatmap: ^1.3.1 @@ -21853,18 +22334,18 @@ __metadata: string.prototype.matchall: ^4.0.8 peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - checksum: e20eab61161a3db6211c2bd1eb9be3e407fd14e72c06c5f39a078b6ac37427b2af6056ee70e3954249bca0a04088ae797a0c8ba909fb8802e29712de2a41262d + checksum: 2232b3b8945aa50b7773919c15cd96892acf35d2f82503667a79e2f55def90f728ed4f0e496f0f157acbe1bd4397c5615b676ae7428fe84488a544ca53feb944 languageName: node linkType: hard "eslint-plugin-testing-library@npm:^5.9.1": - version: 5.9.1 - resolution: "eslint-plugin-testing-library@npm:5.9.1" + version: 5.10.2 + resolution: "eslint-plugin-testing-library@npm:5.10.2" dependencies: - "@typescript-eslint/utils": ^5.13.0 + "@typescript-eslint/utils": ^5.43.0 peerDependencies: eslint: ^7.5.0 || ^8.0.0 - checksum: d09f9486945807e9587d52b6979117bc41b750df741567381a06219671096afb318696a0e0db63e253e150fead40e77ef9653ee00f1dda83fc8920e3b3c47107 + checksum: 3b2b330e62f4a6dc438050006f0d0c97605f6861828b153271dc6d2fafb1e60f4e86fbaa8166c7afd452e3b6cad39413738fd4c8e2eb2def1915c678154676da languageName: node linkType: hard @@ -21929,9 +22410,9 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.6.0": - version: 8.31.0 - resolution: "eslint@npm:8.31.0" +"eslint@npm:^8.33.0, eslint@npm:^8.6.0": + version: 8.33.0 + resolution: "eslint@npm:8.33.0" dependencies: "@eslint/eslintrc": ^1.4.1 "@humanwhocodes/config-array": ^0.11.8 @@ -21974,7 +22455,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 5e5688bb864edc6b12d165849994812eefa67fb3fc44bb26f53659b63edcd8bcc68389d27cc6cc9e5b79ee22f24b6f311fa3ed047bddcafdec7d84c1b5561e4f + checksum: 727e63ab8b7acf281442323c5971f6afdd5b656fbcebc4476cf54e35af51b2f180617433fc5e1952f0449ca3f43a905527f9407ea4b8a7ea7562fc9c3f278d4c languageName: node linkType: hard @@ -22188,6 +22669,7 @@ __metadata: "@backstage/plugin-code-coverage": "workspace:^" "@backstage/plugin-cost-insights": "workspace:^" "@backstage/plugin-dynatrace": "workspace:^" + "@backstage/plugin-entity-feedback": "workspace:^" "@backstage/plugin-explore": "workspace:^" "@backstage/plugin-gcalendar": "workspace:^" "@backstage/plugin-gcp-projects": "workspace:^" @@ -22199,6 +22681,9 @@ __metadata: "@backstage/plugin-kafka": "workspace:^" "@backstage/plugin-kubernetes": "workspace:^" "@backstage/plugin-lighthouse": "workspace:^" + "@backstage/plugin-linguist": "workspace:^" + "@backstage/plugin-linguist-common": "workspace:^" + "@backstage/plugin-microsoft-calendar": "workspace:^" "@backstage/plugin-newrelic": "workspace:^" "@backstage/plugin-newrelic-dashboard": "workspace:^" "@backstage/plugin-org": "workspace:^" @@ -22214,6 +22699,7 @@ __metadata: "@backstage/plugin-sentry": "workspace:^" "@backstage/plugin-shortcuts": "workspace:^" "@backstage/plugin-stack-overflow": "workspace:^" + "@backstage/plugin-stackstorm": "workspace:^" "@backstage/plugin-tech-insights": "workspace:^" "@backstage/plugin-tech-radar": "workspace:^" "@backstage/plugin-techdocs": "workspace:^" @@ -22265,6 +22751,7 @@ __metadata: "@backstage/plugin-app-backend": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-todo-backend": "workspace:^" languageName: unknown linkType: soft @@ -22289,6 +22776,7 @@ __metadata: "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-code-coverage-backend": "workspace:^" + "@backstage/plugin-entity-feedback-backend": "workspace:^" "@backstage/plugin-events-backend": "workspace:^" "@backstage/plugin-events-node": "workspace:^" "@backstage/plugin-explore-backend": "workspace:^" @@ -22296,6 +22784,8 @@ __metadata: "@backstage/plugin-jenkins-backend": "workspace:^" "@backstage/plugin-kafka-backend": "workspace:^" "@backstage/plugin-kubernetes-backend": "workspace:^" + "@backstage/plugin-lighthouse-backend": "workspace:^" + "@backstage/plugin-linguist-backend": "workspace:^" "@backstage/plugin-permission-backend": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" @@ -22352,23 +22842,6 @@ __metadata: languageName: node linkType: hard -"execa@npm:5.0.0": - version: 5.0.0 - resolution: "execa@npm:5.0.0" - dependencies: - cross-spawn: ^7.0.3 - get-stream: ^6.0.0 - human-signals: ^2.1.0 - is-stream: ^2.0.0 - merge-stream: ^2.0.0 - npm-run-path: ^4.0.1 - onetime: ^5.1.2 - signal-exit: ^3.0.3 - strip-final-newline: ^2.0.0 - checksum: a044367ebdcc68ca019810cb134510fc77bbc55c799122258ee0e00e289c132941ab48c2a331a036699c42bc8d479d451ae67c105fce5ce5cc813e7dd92d642b - languageName: node - linkType: hard - "execa@npm:5.1.1, execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -22412,13 +22885,6 @@ __metadata: languageName: node linkType: hard -"exif-parser@npm:^0.1.12": - version: 0.1.12 - resolution: "exif-parser@npm:0.1.12" - checksum: 6ba50cb9e0b45a6efa37e966a9582ecd171b5c5b3ef0c47542f2b862c521f70d2f656dde85b4d2a5dd8e1163486b09049f4c412e9c6176bfbda1654a5b2f021c - languageName: node - linkType: hard - "exit-on-epipe@npm:~1.0.1": version: 1.0.1 resolution: "exit-on-epipe@npm:1.0.1" @@ -22478,7 +22944,7 @@ __metadata: languageName: node linkType: hard -"express-promise-router@npm:^4.1.0, express-promise-router@npm:^4.1.1": +"express-promise-router@npm:^4.1.0": version: 4.1.1 resolution: "express-promise-router@npm:4.1.1" dependencies: @@ -22520,7 +22986,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.1, express@npm:^4.18.2": +"express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.1": version: 4.18.2 resolution: "express@npm:4.18.2" dependencies: @@ -22650,7 +23116,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9": +"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" dependencies: @@ -22691,6 +23157,15 @@ __metadata: languageName: node linkType: hard +"fast-querystring@npm:^1.1.1": + version: 1.1.1 + resolution: "fast-querystring@npm:1.1.1" + dependencies: + fast-decode-uri-component: ^1.0.1 + checksum: 86d2b75b9b299a552353532fb1a542f09730ee2a61e657d68710971d9a2afc9a3c5c7b7e106b6534f4cc506d2ff1c08ab0fda4ae614b4e7720798c9ac2a88e02 + languageName: node + linkType: hard + "fast-redact@npm:^2.0.0": version: 2.1.0 resolution: "fast-redact@npm:2.1.0" @@ -22719,7 +23194,7 @@ __metadata: languageName: node linkType: hard -"fast-url-parser@npm:1.1.3": +"fast-url-parser@npm:1.1.3, fast-url-parser@npm:^1.1.3": version: 1.1.3 resolution: "fast-url-parser@npm:1.1.3" dependencies: @@ -22728,14 +23203,14 @@ __metadata: languageName: node linkType: hard -"fast-xml-parser@npm:4.0.11": - version: 4.0.11 - resolution: "fast-xml-parser@npm:4.0.11" +"fast-xml-parser@npm:4.1.2": + version: 4.1.2 + resolution: "fast-xml-parser@npm:4.1.2" dependencies: strnum: ^1.0.5 bin: fxparser: src/cli/cli.js - checksum: d8a08e4d5597e0fc00a86735195872eeb03008913e298830941516f3766e16ee555e2d431acc92e1dda887938edc445252ec5b59494aab60a8389888bd13719c + checksum: 6a7d1b17057f8470e70603eddfa75f990625735d068d57ece861d0154ad8d27fda63c2831d07e1ecd7e68e993738b2448925cb9277d8c0ed68009623bbcd63c6 languageName: node linkType: hard @@ -22789,6 +23264,21 @@ __metadata: languageName: node linkType: hard +"fbjs@npm:^0.8.1": + version: 0.8.18 + resolution: "fbjs@npm:0.8.18" + dependencies: + core-js: ^1.0.0 + isomorphic-fetch: ^2.1.1 + loose-envify: ^1.0.0 + object-assign: ^4.1.0 + promise: ^7.1.1 + setimmediate: ^1.0.5 + ua-parser-js: ^0.7.30 + checksum: 668731b946a765908c9cbe51d5160f973abb78004b3d122587c3e930e3e1ddcc0ce2b17f2a8637dc9d733e149aa580f8d3035a35cc2d3bc78b78f1b19aab90e2 + languageName: node + linkType: hard + "fbjs@npm:^3.0.0": version: 3.0.0 resolution: "fbjs@npm:3.0.0" @@ -22849,13 +23339,6 @@ __metadata: languageName: node linkType: hard -"file-type@npm:^9.0.0": - version: 9.0.0 - resolution: "file-type@npm:9.0.0" - checksum: 9ea78b29c3762d967eb1e3e4f45e401388b6d252b12c217f78f5ea97556ff7e35e4c7255cab68810ac414d51b776bd4e83504c86f132c262a454251561189efa - languageName: node - linkType: hard - "file-uri-to-path@npm:1.0.0": version: 1.0.0 resolution: "file-uri-to-path@npm:1.0.0" @@ -22963,15 +23446,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^2.1.0": - version: 2.1.0 - resolution: "find-up@npm:2.1.0" - dependencies: - locate-path: ^2.0.0 - checksum: 43284fe4da09f89011f08e3c32cd38401e786b19226ea440b75386c1b12a4cb738c94969808d53a84f564ede22f732c8409e3cfc3f7fb5b5c32378ad0bbf28bd - languageName: node - linkType: hard - "find-up@npm:^3.0.0": version: 3.0.0 resolution: "find-up@npm:3.0.0" @@ -23067,7 +23541,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.7, follow-redirects@npm:^1.14.8, follow-redirects@npm:^1.14.9": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.8, follow-redirects@npm:^1.14.9": version: 1.15.2 resolution: "follow-redirects@npm:1.15.2" peerDependenciesMeta: @@ -23546,16 +24020,6 @@ __metadata: languageName: node linkType: hard -"get-monorepo-packages@npm:^1.1.0": - version: 1.2.0 - resolution: "get-monorepo-packages@npm:1.2.0" - dependencies: - globby: ^7.1.1 - load-json-file: ^4.0.0 - checksum: f9321c11b8e11f02138758db6589d8ab8b7e1b05e78cac92493b635faea10aa100c20fe40f2cf110c82ec100c118c6c131dff4c65d32a721c617dc2928f7b277 - languageName: node - linkType: hard - "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -23603,6 +24067,13 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.4.0": + version: 4.4.0 + resolution: "get-tsconfig@npm:4.4.0" + checksum: e193558b4f0c84c81ae9688cf5b9950dc0b341e44f91b002713fd0c37cfb73108e1cd9998ed540bcc423f193fde32cc58a15e99dd469f5158a2eb4a148611176 + languageName: node + linkType: hard + "getopts@npm:2.3.0": version: 2.3.0 resolution: "getopts@npm:2.3.0" @@ -23693,7 +24164,20 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.0": +"glob@npm:8.0.3": + version: 8.0.3 + resolution: "glob@npm:8.0.3" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^5.0.1 + once: ^1.3.0 + checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5 + languageName: node + linkType: hard + +"glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.0": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -23708,15 +24192,15 @@ __metadata: linkType: hard "glob@npm:^8.0.0, glob@npm:^8.0.1, glob@npm:^8.0.3": - version: 8.0.3 - resolution: "glob@npm:8.0.3" + version: 8.1.0 + resolution: "glob@npm:8.1.0" dependencies: fs.realpath: ^1.0.0 inflight: ^1.0.4 inherits: 2 minimatch: ^5.0.1 once: ^1.3.0 - checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5 + checksum: 92fbea3221a7d12075f26f0227abac435de868dd0736a17170663783296d0dd8d3d532a5672b4488a439bf5d7fb85cdd07c11185d6cd39184f0385cbdfb86a47 languageName: node linkType: hard @@ -23763,16 +24247,6 @@ __metadata: languageName: node linkType: hard -"global@npm:~4.4.0": - version: 4.4.0 - resolution: "global@npm:4.4.0" - dependencies: - min-document: ^2.19.0 - process: ^0.11.10 - checksum: 9c057557c8f5a5bcfbeb9378ba4fe2255d04679452be504608dd5f13b54edf79f7be1db1031ea06a4ec6edd3b9f5f17d2d172fb47e6c69dae57fd84b7e72b77f - languageName: node - linkType: hard - "globals@npm:^11.1.0, globals@npm:^11.12.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -23812,17 +24286,25 @@ __metadata: languageName: node linkType: hard -"globby@npm:^7.1.1": - version: 7.1.1 - resolution: "globby@npm:7.1.1" +"globby@npm:^13.1.2": + version: 13.1.3 + resolution: "globby@npm:13.1.3" dependencies: - array-union: ^1.0.1 - dir-glob: ^2.0.0 - glob: ^7.1.2 - ignore: ^3.3.5 - pify: ^3.0.0 - slash: ^1.0.0 - checksum: f0eba08a08ae7c98149a4411661c0bf08c4717d81e6f355cf624fb01880b249737eb8e951bf86124cb3af8ea1c793c0a9d363ed5cdec99bb2c6b68f8a323025f + dir-glob: ^3.0.1 + fast-glob: ^3.2.11 + ignore: ^5.2.0 + merge2: ^1.4.1 + slash: ^4.0.0 + checksum: 93f06e02002cdf368f7e3d55bd59e7b00784c7cc8fe92c7ee5082cc7171ff6109fda45e1c97a80bb48bc811dedaf7843c7c9186f5f84bde4883ab630e13c43df + languageName: node + linkType: hard + +"good-listener@npm:^1.2.2": + version: 1.2.2 + resolution: "good-listener@npm:1.2.2" + dependencies: + delegate: ^3.1.2 + checksum: f39fb82c4e41524f56104cfd2d7aef1a88e72f3f75139115fbdf98cc7d844e0c1b39218b2e83438c6188727bf904ed78c7f0f2feff67b32833bc3af7f0202b33 languageName: node linkType: hard @@ -23843,13 +24325,14 @@ __metadata: languageName: node linkType: hard -"google-gax@npm:^3.5.2": - version: 3.5.2 - resolution: "google-gax@npm:3.5.2" +"google-gax@npm:^3.5.2, google-gax@npm:^3.5.3": + version: 3.5.7 + resolution: "google-gax@npm:3.5.7" dependencies: - "@grpc/grpc-js": ~1.7.0 + "@grpc/grpc-js": ~1.8.0 "@grpc/proto-loader": ^0.7.0 "@types/long": ^4.0.0 + "@types/rimraf": ^3.0.2 abort-controller: ^3.0.0 duplexify: ^4.0.0 fast-text-encoding: ^1.0.3 @@ -23858,13 +24341,13 @@ __metadata: node-fetch: ^2.6.1 object-hash: ^3.0.0 proto3-json-serializer: ^1.0.0 - protobufjs: 7.1.2 - protobufjs-cli: 1.0.2 + protobufjs: 7.2.2 + protobufjs-cli: 1.1.1 retry-request: ^5.0.0 bin: compileProtos: build/tools/compileProtos.js minifyProtoJson: build/tools/minify.js - checksum: 619a961acba742c30eaba85f5e0fb5903f8fc6713e26bf1736549cbbd43d7fac3ba3379dbc96850117263199ea8e3e74635b078e9464c7fadbbcba8822949135 + checksum: 1f3c647fc1ddc00a6a017be0f83b89691997358720763db13a69a94c7a74f6d31a49b0fd56f7bab18689914c784328901b118bdf150d742719995c705d0fa65d languageName: node linkType: hard @@ -23914,7 +24397,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.10 resolution: "graceful-fs@npm:4.2.10" checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da @@ -23954,7 +24437,7 @@ __metadata: languageName: node linkType: hard -"graphql-config@npm:4.4.0": +"graphql-config@npm:^4.4.0": version: 4.4.0 resolution: "graphql-config@npm:4.4.0" dependencies: @@ -24038,6 +24521,27 @@ __metadata: languageName: node linkType: hard +"graphql-voyager@npm:^1.0.0-rc.31": + version: 1.0.0-rc.31 + resolution: "graphql-voyager@npm:1.0.0-rc.31" + dependencies: + "@f/animate": ^1.0.1 + "@material-ui/core": ^3.9.3 + classnames: ^2.2.6 + clipboard: ^2.0.4 + commonmark: ^0.29.0 + lodash: ^4.17.10 + prop-types: ^15.7.2 + svg-pan-zoom: ^3.6.0 + viz.js: 2.1.2 + peerDependencies: + graphql: ">=14.0.0" + react: ">=15.4.2" + react-dom: ">=15.4.2" + checksum: 80e826eeb42d286540447723ea35a463a3a155287914a45e24b69f31617d8ca74c0c43bdaa64123bf4ed046e68f7e31feca7661c06288551040f3e5b96c48fed + languageName: node + linkType: hard + "graphql-ws@npm:5.11.2, graphql-ws@npm:^5.4.1, graphql-ws@npm:^5.9.0": version: 5.11.2 resolution: "graphql-ws@npm:5.11.2" @@ -24047,7 +24551,7 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^15.0.0 || ^16.0.0, graphql@npm:^16.0.0, graphql@npm:^16.3.0": +"graphql@npm:^15.0.0 || ^16.0.0, graphql@npm:^16.0.0": version: 16.6.0 resolution: "graphql@npm:16.6.0" checksum: bf1d9e3c1938ce3c1a81e909bd3ead1ae4707c577f91cff1ca2eca474bfbc7873d5d7b942e1e9777ff5a8304421dba57a4b76d7a29eb19de8711cb70e3c2415e @@ -24168,22 +24672,6 @@ __metadata: languageName: node linkType: hard -"has-binary2@npm:~1.0.2": - version: 1.0.3 - resolution: "has-binary2@npm:1.0.3" - dependencies: - isarray: 2.0.1 - checksum: 9183a617830b1f50b35961fbe39278cc43089cdac7c47a7f16aafb9d8190553fa360e5da34a479d6c988db40ea10aa02bca5abcb2451b5fc3924ae2f013ffdab - languageName: node - linkType: hard - -"has-cors@npm:1.1.0": - version: 1.1.0 - resolution: "has-cors@npm:1.1.0" - checksum: 549ce94113fd23895b22d71ade9809918577b8558cd4d701fe79045d8b1d58d87eba870260b28f6a3229be933a691c55653afd496d0fc52e98fd2ff577f01197 - languageName: node - linkType: hard - "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" @@ -24312,7 +24800,7 @@ __metadata: languageName: node linkType: hard -"headers-polyfill@npm:^3.0.4, headers-polyfill@npm:^3.1.0": +"headers-polyfill@npm:^3.1.0": version: 3.1.0 resolution: "headers-polyfill@npm:3.1.0" checksum: a95257065684653b7185efbb9a380c547ea832002991b5adf0d90cd222073da2701be9dc2849d1970ecf15e8c35b383984358566afe6e76ca8ff1dbd7cdce3af @@ -24360,19 +24848,14 @@ __metadata: languageName: node linkType: hard -"hogan.js@npm:^3.0.2": - version: 3.0.2 - resolution: "hogan.js@npm:3.0.2" - dependencies: - mkdirp: 0.3.0 - nopt: 1.0.10 - bin: - hulk: ./bin/hulk - checksum: c7bbff84faa9ca265c39f4a2100546ba0388fcc9c5bac8526f488592ce3fcaa042eba6ac25db277f4478ec3855b9bc28ce59acffbf6e8a28d45a17df7590c6aa +"hoist-non-react-statics@npm:^2.3.1": + version: 2.5.5 + resolution: "hoist-non-react-statics@npm:2.5.5" + checksum: ee2d05e5c7e1398ad84a15b0327f66bd78f38a8e0015e852f954b36434e32eb7e942d5357505020a3a1147f247b165bf1e69d72393e3accab67cafdafeb86230 languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": +"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.2.1, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -24423,6 +24906,13 @@ __metadata: languageName: node linkType: hard +"hpagent@npm:^1.2.0": + version: 1.2.0 + resolution: "hpagent@npm:1.2.0" + checksum: b029da695edae438cee4da2a437386f9db4ac27b3ceb7306d02e1b586c9c194741ed2e943c8a222e0cfefaf27ee3f863aca7ba1721b0950a2a19bf25bc0d85e2 + languageName: node + linkType: hard + "html-encoding-sniffer@npm:^2.0.1": version: 2.0.1 resolution: "html-encoding-sniffer@npm:2.0.1" @@ -24512,9 +25002,9 @@ __metadata: linkType: hard "http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0": - version: 4.1.0 - resolution: "http-cache-semantics@npm:4.1.0" - checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 languageName: node linkType: hard @@ -24686,9 +25176,9 @@ __metadata: linkType: hard "humanize-duration@npm:^3.25.1, humanize-duration@npm:^3.26.0, humanize-duration@npm:^3.27.0, humanize-duration@npm:^3.27.1": - version: 3.27.3 - resolution: "humanize-duration@npm:3.27.3" - checksum: 17193537334676cbff156a5f60ea6349e4e981f33062fdae901ac10ed096d4f05e7b8839d362cbda7a06ade2a499f0a655e3c1bf9da6d4a234d99ed0498ba516 + version: 3.28.0 + resolution: "humanize-duration@npm:3.28.0" + checksum: dde595ab4729d82147db41d8ca0afa86bf3f20d74cd1323ea52f299091a4f1dcb2a45b2d27ce78ab9bb0d9e66d8eb8f2628a67be36a535af645eb282a963ff7f languageName: node linkType: hard @@ -24799,26 +25289,10 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^3.3.5": - version: 3.3.10 - resolution: "ignore@npm:3.3.10" - checksum: 23e8cc776e367b56615ab21b78decf973a35dfca5522b39d9b47643d8168473b0d1f18dd1321a1bab466a12ea11a2411903f3b21644f4d5461ee0711ec8678bd - languageName: node - linkType: hard - -"ignore@npm:^5.1.4, ignore@npm:^5.2.0": - version: 5.2.0 - resolution: "ignore@npm:5.2.0" - checksum: 6b1f926792d614f64c6c83da3a1f9c83f6196c2839aa41e1e32dd7b8d174cef2e329d75caabb62cb61ce9dc432f75e67d07d122a037312db7caa73166a1bdb77 - languageName: node - linkType: hard - -"image-size@npm:^0.7.2": - version: 0.7.5 - resolution: "image-size@npm:0.7.5" - bin: - image-size: bin/image-size.js - checksum: f88860c9d9b5c8ad00d3de9d6f5ba105bda5a5024bfb6b90559a075a4b838ed4f5d3cba14edf0f18fe5d75df596a172b52feca43848e11c34f31f4df2c88a011 +"ignore@npm:^5.1.4, ignore@npm:^5.2.0, ignore@npm:^5.2.4": + version: 5.2.4 + resolution: "ignore@npm:5.2.4" + checksum: 3d4c309c6006e2621659311783eaea7ebcd41fe4ca1d78c91c473157ad6666a57a2df790fe0d07a12300d9aac2888204d7be8d59f9aaf665b1c7fcdb432517ef languageName: node linkType: hard @@ -24830,9 +25304,9 @@ __metadata: linkType: hard "immer@npm:^9.0.1, immer@npm:^9.0.7": - version: 9.0.18 - resolution: "immer@npm:9.0.18" - checksum: 85b3153dd01fce73c40591d0d6d7cd95878dab49f8ec4744c044adac05e0dab847b30c26259c478a5f87f974897be6df2780ff5bf86c8a0a27578fdeb300eb10 + version: 9.0.19 + resolution: "immer@npm:9.0.19" + checksum: f02ee53989989c287cd548a3d817fccf0bfe56db919755ee94a72ea3ae78a00363fba93ee6c010fe54a664380c29c53d44ed4091c6a86cae60957ad2cfabc010 languageName: node linkType: hard @@ -24911,6 +25385,15 @@ __metadata: languageName: node linkType: hard +"indefinite-observable@npm:^1.0.1": + version: 1.0.2 + resolution: "indefinite-observable@npm:1.0.2" + dependencies: + symbol-observable: 1.2.0 + checksum: 69a337967f48fca18989f9d68ad98c7220ed9b499bf00330ff72669a9583cb7f8e82f801da10720f720edf1313f427c77ce793350bdc413c952cec8ce112fc12 + languageName: node + linkType: hard + "indefinite-observable@npm:^2.0.1": version: 2.0.1 resolution: "indefinite-observable@npm:2.0.1" @@ -24927,10 +25410,10 @@ __metadata: languageName: node linkType: hard -"indexof@npm:0.0.1": - version: 0.0.1 - resolution: "indexof@npm:0.0.1" - checksum: 0fb04e8b147b8585d981a6df1564f25bb3678d6fa74e33e5cecc1464b10f78e15e8ef6bb688f135fe5c2844a128fac8a7831cbe5adc81fdcf12681b093dfcc25 +"indent-string@npm:^5.0.0": + version: 5.0.0 + resolution: "indent-string@npm:5.0.0" + checksum: e466c27b6373440e6d84fbc19e750219ce25865cb82d578e41a6053d727e5520dc5725217d6eb1cc76005a1bb1696a0f106d84ce7ebda3033b963a38583fb3b3 languageName: node linkType: hard @@ -25105,20 +25588,20 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:^5.2.4": - version: 5.2.4 - resolution: "ioredis@npm:5.2.4" +"ioredis@npm:^5.3.0": + version: 5.3.0 + resolution: "ioredis@npm:5.3.0" dependencies: "@ioredis/commands": ^1.1.1 cluster-key-slot: ^1.1.0 debug: ^4.3.4 - denque: ^2.0.1 + denque: ^2.1.0 lodash.defaults: ^4.2.0 lodash.isarguments: ^3.1.0 redis-errors: ^1.2.0 redis-parser: ^3.0.0 standard-as-callback: ^2.1.0 - checksum: c3a7df407a41ae516bede8b6db853c568198b13fe4a4785f66be5cd541087121b9d45fb9a6b1b6a5fb668c29ce52ab4685642b994803bdfa0a35f794ea8ef7ae + checksum: d3a1fd8f1b5d9ce6d4b7dd083773dbe0936ea3d1f55f07d36daa3d0b59c4636e95640f90bc003a08bc900a9803307e7d7e191817a2c2970696e204dae6f3dc35 languageName: node linkType: hard @@ -25309,12 +25792,12 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.1.0, is-core-module@npm:^2.8.1, is-core-module@npm:^2.9.0": - version: 2.10.0 - resolution: "is-core-module@npm:2.10.0" +"is-core-module@npm:^2.1.0, is-core-module@npm:^2.11.0, is-core-module@npm:^2.9.0": + version: 2.11.0 + resolution: "is-core-module@npm:2.11.0" dependencies: has: ^1.0.3 - checksum: 0f3f77811f430af3256fa7bbc806f9639534b140f8ee69476f632c3e1eb4e28a38be0b9d1b8ecf596179c841b53576129279df95e7051d694dac4ceb6f967593 + checksum: f96fd490c6b48eb4f6d10ba815c6ef13f410b0ba6f7eb8577af51697de523e5f2cd9de1c441b51d27251bf0e4aebc936545e33a5d26d5d51f28d25698d4a8bab languageName: node linkType: hard @@ -25380,13 +25863,6 @@ __metadata: languageName: node linkType: hard -"is-function@npm:^1.0.1": - version: 1.0.2 - resolution: "is-function@npm:1.0.2" - checksum: 7d564562e07b4b51359547d3ccc10fb93bb392fd1b8177ae2601ee4982a0ece86d952323fc172a9000743a3971f09689495ab78a1d49a9b14fc97a7e28521dc0 - languageName: node - linkType: hard - "is-generator-fn@npm:^2.0.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" @@ -25508,10 +25984,10 @@ __metadata: languageName: node linkType: hard -"is-path-cwd@npm:^2.2.0": - version: 2.2.0 - resolution: "is-path-cwd@npm:2.2.0" - checksum: 46a840921bb8cc0dc7b5b423a14220e7db338072a4495743a8230533ce78812dc152548c86f4b828411fe98c5451959f07cf841c6a19f611e46600bd699e8048 +"is-path-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "is-path-cwd@npm:3.0.0" + checksum: bc34d13b6a03dfca4a3ab6a8a5ba78ae4b24f4f1db4b2b031d2760c60d0913bd16a4b980dcb4e590adfc906649d5f5132684079a3972bd219da49deebb9adea8 languageName: node linkType: hard @@ -25522,6 +25998,13 @@ __metadata: languageName: node linkType: hard +"is-path-inside@npm:^4.0.0": + version: 4.0.0 + resolution: "is-path-inside@npm:4.0.0" + checksum: 8810fa11c58e6360b82c3e0d6cd7d9c7d0392d3ac9eb10f980b81f9839f40ac6d1d6d6f05d069db0d227759801228f0b072e1b6c343e4469b065ab5fe0b68fe5 + languageName: node + linkType: hard + "is-plain-obj@npm:^1.1.0": version: 1.1.0 resolution: "is-plain-obj@npm:1.1.0" @@ -25677,6 +26160,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^1.0.1": + version: 1.1.0 + resolution: "is-stream@npm:1.1.0" + checksum: 063c6bec9d5647aa6d42108d4c59723d2bd4ae42135a2d4db6eadbd49b7ea05b750fd69d279e5c7c45cf9da753ad2c00d8978be354d65aa9f6bb434969c6a2ae + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -25819,13 +26309,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:2.0.1": - version: 2.0.1 - resolution: "isarray@npm:2.0.1" - checksum: a0521973213019b82889f028a41528ce7a3f103a710357b6875066202ee5a1fa9ae4135f915f3be9160b8c7a3ae4b39b073b804da3b58fa6b72f8b4b1e3dfef5 - languageName: node - linkType: hard - "isarray@npm:^1.0.0, isarray@npm:~1.0.0": version: 1.0.0 resolution: "isarray@npm:1.0.0" @@ -25840,7 +26323,7 @@ __metadata: languageName: node linkType: hard -"isbinaryfile@npm:^4.0.10, isbinaryfile@npm:^4.0.8": +"isbinaryfile@npm:^4.0.10, isbinaryfile@npm:^4.0.8, isbinaryfile@npm:^4.0.8 <5": version: 4.0.10 resolution: "isbinaryfile@npm:4.0.10" checksum: a6b28db7e23ac7a77d3707567cac81356ea18bd602a4f21f424f862a31d0e7ab4f250759c98a559ece35ffe4d99f0d339f1ab884ffa9795172f632ab8f88e686 @@ -25879,6 +26362,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-fetch@npm:^2.1.1": + version: 2.2.1 + resolution: "isomorphic-fetch@npm:2.2.1" + dependencies: + node-fetch: ^1.0.1 + whatwg-fetch: ">=0.10.0" + checksum: bb5daa7c3785d6742f4379a81e55b549a469503f7c9bf9411b48592e86632cf5e8fe8ea878dba185c0f33eb7c510c23abdeb55aebfdf5d3c70f031ced68c5424 + languageName: node + linkType: hard + "isomorphic-fetch@npm:^3.0.0": version: 3.0.0 resolution: "isomorphic-fetch@npm:3.0.0" @@ -25919,7 +26412,7 @@ __metadata: languageName: node linkType: hard -"isomorphic-ws@npm:5.0.0": +"isomorphic-ws@npm:5.0.0, isomorphic-ws@npm:^5.0.0": version: 5.0.0 resolution: "isomorphic-ws@npm:5.0.0" peerDependencies: @@ -25928,15 +26421,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-ws@npm:^4.0.1": - version: 4.0.1 - resolution: "isomorphic-ws@npm:4.0.1" - peerDependencies: - ws: "*" - checksum: d7190eadefdc28bdb93d67b5f0c603385aaf87724fa2974abb382ac1ec9756ed2cfb27065cbe76122879c2d452e2982bc4314317f3d6c737ddda6c047328771a - languageName: node - linkType: hard - "isstream@npm:~0.1.2": version: 0.1.2 resolution: "isstream@npm:0.1.2" @@ -26598,20 +27082,6 @@ __metadata: languageName: node linkType: hard -"jimp@npm:^0.10.3": - version: 0.10.3 - resolution: "jimp@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/custom": ^0.10.3 - "@jimp/plugins": ^0.10.3 - "@jimp/types": ^0.10.3 - core-js: ^3.4.1 - regenerator-runtime: ^0.13.3 - checksum: ace7d9d76c2802e26392603c1fdfc988e7c468941ac127405f6752db9b5c89e1e27f202f96b764eede001ac245be0b46f52bb87eb91a42374027050568a7e816 - languageName: node - linkType: hard - "jju@npm:~1.4.0": version: 1.4.0 resolution: "jju@npm:1.4.0" @@ -26633,7 +27103,7 @@ __metadata: languageName: node linkType: hard -"joi@npm:^17.6.0": +"joi@npm:^17.7.0": version: 17.7.0 resolution: "joi@npm:17.7.0" dependencies: @@ -26647,9 +27117,9 @@ __metadata: linkType: hard "jose@npm:^4.10.0, jose@npm:^4.6.0": - version: 4.11.2 - resolution: "jose@npm:4.11.2" - checksum: bc9b2271c31683603857b2fd2903afa29f826fffd4bcefc0845355ed86995def87eaadf502033207996109d76698c912e5a986e279326d9629321a50504fe466 + version: 4.12.0 + resolution: "jose@npm:4.12.0" + checksum: 09e67611768127ab54b6b507401de4b1f87e1e285cf2c2fc917e931e001b7e584c90081b421f483f13a6eec4fc44936e4a5f4b8ae2d59928061e886e35d33fa2 languageName: node linkType: hard @@ -26660,24 +27130,10 @@ __metadata: languageName: node linkType: hard -"jpeg-js@npm:^0.3.4": - version: 0.3.7 - resolution: "jpeg-js@npm:0.3.7" - checksum: 85a1ab09fe696fdd7b94ee077f1b8d1de307ffd7a83c0c45cc5cc56b0d7b47e561b92f6f090165648387875d674ba3076e3fedf395d5be4ef46760b6ebc804fe - languageName: node - linkType: hard - -"js-base64@npm:^2.5.1": - version: 2.6.4 - resolution: "js-base64@npm:2.6.4" - checksum: 5f4084078d6c46f8529741d110df84b14fac3276b903760c21fa8cc8521370d607325dfe1c1a9fbbeaae1ff8e602665aaeef1362427d8fef704f9e3659472ce8 - languageName: node - linkType: hard - "js-base64@npm:^3.6.0": - version: 3.7.4 - resolution: "js-base64@npm:3.7.4" - checksum: b8f6b10033b57bbb87fa34629f44bc665cb8b7bcba49610a29b296592616e633e9c186afaa02de5a54d60c08aaa36c8c8ad585db095a96fb331fe022f54792a2 + version: 3.7.5 + resolution: "js-base64@npm:3.7.5" + checksum: 67a78c8b1c47b73f1c6fba1957e9fe6fd9dc78ac93ac46cc2e43472dcb9cf150d126fb0e593192e88e0497354fa634d17d255add7cc6ee3c7b4d29870faa8e18 languageName: node linkType: hard @@ -26819,11 +27275,12 @@ __metadata: languageName: node linkType: hard -"jsdoc@npm:^3.6.3": - version: 3.6.11 - resolution: "jsdoc@npm:3.6.11" +"jsdoc@npm:^4.0.0": + version: 4.0.2 + resolution: "jsdoc@npm:4.0.2" dependencies: - "@babel/parser": ^7.9.4 + "@babel/parser": ^7.20.15 + "@jsdoc/salty": ^0.2.1 "@types/markdown-it": ^12.2.3 bluebird: ^3.7.2 catharsis: ^0.9.0 @@ -26836,11 +27293,10 @@ __metadata: mkdirp: ^1.0.4 requizzle: ^0.2.3 strip-json-comments: ^3.1.0 - taffydb: 2.6.2 underscore: ~1.13.2 bin: jsdoc: jsdoc.js - checksum: 7920b5cba6200c8f56c9ac2ac5e89d06b6581dd1ce22e66976409fad8b68c16bfd8cd30fe9af58baeacc9f6bd9ba06d901ca4f5e234944f42a3c37e55e4ddcf0 + checksum: 04bf5ab005349b7581bd0e72ed99933eb71a41dcb47235b486b7d9146fbdf212a53e0cc044abe48ccf46012bd812dc1dfc007c6d679660ebdd053cd000242515 languageName: node linkType: hard @@ -26958,13 +27414,6 @@ __metadata: languageName: node linkType: hard -"json-parse-better-errors@npm:^1.0.1": - version: 1.0.2 - resolution: "json-parse-better-errors@npm:1.0.2" - checksum: ff2b5ba2a70e88fd97a3cb28c1840144c5ce8fae9cbeeddba15afa333a5c407cf0e42300cd0a2885dbb055227fe68d405070faad941beeffbfde9cf3b2c78c5d - languageName: node - linkType: hard - "json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -27004,15 +27453,15 @@ __metadata: linkType: hard "json-schema-library@npm:^7.3.9": - version: 7.4.4 - resolution: "json-schema-library@npm:7.4.4" + version: 7.4.7 + resolution: "json-schema-library@npm:7.4.7" dependencies: "@sagold/json-pointer": ^5.0.0 "@sagold/json-query": ^6.0.0 deepmerge: ^4.2.2 fast-deep-equal: ^3.1.3 valid-url: ^1.0.9 - checksum: e485c7f40bcfe04b96f457e02bc4ecb5bfe6b1d91df6a788909d1048a48bd51ecc5fe6bdd8759289a95c37fa63b3347b29d6d93f1b26a47f40cff21981d331d5 + checksum: e738f0b149d52949d854a4de718e79576fabbae206bffdb1cf895ed0b87bbf255f80f162f1924fb89311f6ac1a8e3ed0a5940b709bc3a091ad217f64de70cf60 languageName: node linkType: hard @@ -27117,13 +27566,13 @@ __metadata: linkType: hard "json5@npm:^1.0.1": - version: 1.0.1 - resolution: "json5@npm:1.0.1" + version: 1.0.2 + resolution: "json5@npm:1.0.2" dependencies: minimist: ^1.2.0 bin: json5: lib/cli.js - checksum: e76ea23dbb8fc1348c143da628134a98adf4c5a4e8ea2adaa74a80c455fc2cdf0e2e13e6398ef819bfe92306b610ebb2002668ed9fc1af386d593691ef346fc3 + checksum: 866458a8c58a95a49bef3adba929c625e82532bcff1fe93f01d29cb02cac7c3fe1f4b79951b7792c2da9de0b32871a8401a6e3c5b36778ad852bf5b8a61165d7 languageName: node linkType: hard @@ -27182,13 +27631,6 @@ __metadata: languageName: node linkType: hard -"jsonpath-plus@npm:^0.19.0": - version: 0.19.0 - resolution: "jsonpath-plus@npm:0.19.0" - checksum: defd19990df6e45cb45c4c1c5185139995a5ead40967acc373b32375ace83d329bb96c0b69c0ae9a8c581b6c4bdb82f8b56766053c9251d4d9158161f1ba8d70 - languageName: node - linkType: hard - "jsonpath-plus@npm:^5.0.7": version: 5.1.0 resolution: "jsonpath-plus@npm:5.1.0" @@ -27196,6 +27638,13 @@ __metadata: languageName: node linkType: hard +"jsonpath-plus@npm:^7.2.0": + version: 7.2.0 + resolution: "jsonpath-plus@npm:7.2.0" + checksum: 05f447339d29be861e307d6e812aec1b9b88a3ba6bba286966a4e8bed3e752bee3d715eabfc21dce968be85ccb48bf79d2c1af78da7b9b74cd1b446d4d5d02f5 + languageName: node + linkType: hard + "jsonpointer@npm:^5.0.0, jsonpointer@npm:^5.0.1": version: 5.0.1 resolution: "jsonpointer@npm:5.0.1" @@ -27246,6 +27695,46 @@ __metadata: languageName: node linkType: hard +"jss-camel-case@npm:^6.0.0": + version: 6.1.0 + resolution: "jss-camel-case@npm:6.1.0" + dependencies: + hyphenate-style-name: ^1.0.2 + peerDependencies: + jss: ^9.7.0 + checksum: f20ad892cddc30241b5127d648233b513ce57b2a6ed2f710cd995510f8d06e1418cd8f6e8e50dc6bd60b2e8a35159bfdeb40143938e0f2cdc19fd39279953789 + languageName: node + linkType: hard + +"jss-default-unit@npm:^8.0.2": + version: 8.0.2 + resolution: "jss-default-unit@npm:8.0.2" + peerDependencies: + jss: ^9.4.0 + checksum: 5277c5ccc3d56f5c137d6d65f0fc36d15eb22ef08b5949e887ee75e9f4f25197e25f2a99706b70a2828cee010fd8ed7484fb1bb2086fe327f2b24b1bdcc22abb + languageName: node + linkType: hard + +"jss-global@npm:^3.0.0": + version: 3.0.0 + resolution: "jss-global@npm:3.0.0" + peerDependencies: + jss: ^9.0.0 + checksum: e3fa80d8251ba5f183d9b0b4416d64e8f5d285cfdb595cc600daf1a1366b67bca573939bb71d98b6596bf4d6f2c95711cf53a358af3d63cd7945dbb434c6547b + languageName: node + linkType: hard + +"jss-nested@npm:^6.0.1": + version: 6.0.1 + resolution: "jss-nested@npm:6.0.1" + dependencies: + warning: ^3.0.0 + peerDependencies: + jss: ^9.0.0 + checksum: 437bdacc559be0b4b5bc1faa2bc77b5c6cf14733fefbf73a34bb7335786b9f08e4e79b3d73cf83b386959adcd8fa9d725877912e96d9a0921ed38baa1a69b8d9 + languageName: node + linkType: hard + "jss-plugin-camel-case@npm:^10.5.1": version: 10.6.0 resolution: "jss-plugin-camel-case@npm:10.6.0" @@ -27320,6 +27809,26 @@ __metadata: languageName: node linkType: hard +"jss-props-sort@npm:^6.0.0": + version: 6.0.0 + resolution: "jss-props-sort@npm:6.0.0" + peerDependencies: + jss: ^9.0.0 + checksum: 82a04f625a2f2b3a71b2fcb00b1ed0478137c83dc47c323d24c7ea88d08c9ea295b061f99cf264db133fef3435366d6da594724f7713e0415dc50b4eff2aeb53 + languageName: node + linkType: hard + +"jss-vendor-prefixer@npm:^7.0.0": + version: 7.0.0 + resolution: "jss-vendor-prefixer@npm:7.0.0" + dependencies: + css-vendor: ^0.3.8 + peerDependencies: + jss: ^9.0.0 + checksum: 8ec3608711833e79da7ccfffd8177e752f16093589fa28d3a24ec19e6588fbc6a07cc5cacf59ef4c111ab1d0fb72e07b88cb3444eb8c0c6ed4cc8107da984633 + languageName: node + linkType: hard + "jss@npm:10.6.0": version: 10.6.0 resolution: "jss@npm:10.6.0" @@ -27333,15 +27842,26 @@ __metadata: languageName: node linkType: hard -"jss@npm:^10.5.1, jss@npm:~10.9.0": - version: 10.9.2 - resolution: "jss@npm:10.9.2" +"jss@npm:^10.5.1, jss@npm:~10.10.0": + version: 10.10.0 + resolution: "jss@npm:10.10.0" dependencies: "@babel/runtime": ^7.3.1 csstype: ^3.0.2 is-in-browser: ^1.1.3 tiny-warning: ^1.0.2 - checksum: 7ae5cd2f8602bf197ec90251d774b9f10d55eb2db0854ac78dc7fb6983828c202e8eb0d5c8c59c73b2f64718ebd33d6063afa799d625a995986a22dc1cc27230 + checksum: ecf71971df42729668c283e432e841349b7fdbe52e520f7704991cf4a738fd2451ec0feeb25c12cdc5addf7facecf838e74e62936fd461fb4c99f23d54a4792d + languageName: node + linkType: hard + +"jss@npm:^9.8.7": + version: 9.8.7 + resolution: "jss@npm:9.8.7" + dependencies: + is-in-browser: ^1.1.3 + symbol-observable: ^1.1.0 + warning: ^3.0.0 + checksum: ebb264cc893fb8c17a0277875947f8f72e01f5be991bf7f61a39abb861e1276e75ee402fdd2be9ce827af839a1a08bd81ad86a4e108d57fd7d7ebb2361837a53 languageName: node linkType: hard @@ -27481,8 +28001,8 @@ __metadata: linkType: hard "knex@npm:^2.0.0": - version: 2.4.0 - resolution: "knex@npm:2.4.0" + version: 2.4.2 + resolution: "knex@npm:2.4.2" dependencies: colorette: 2.0.19 commander: ^9.1.0 @@ -27515,7 +28035,7 @@ __metadata: optional: true bin: knex: bin/cli.js - checksum: 66b4ae8412c61125b78512a66d924af95116628e16026efce4d03b80b2ced6b63208b37bb49fb52a499761da5e8981e136b48fcefcd48c3fe9fc72d9d17c5661 + checksum: cfb6436c0e3df3cd1f55d3637e4f222d1acbc7b6ec3757e52c92cbf0a7da4055d40156d707ffede57dc155af75d7f5739a49d1b31d8755bc77bc0e4e2a570748 languageName: node linkType: hard @@ -27676,6 +28196,25 @@ __metadata: languageName: node linkType: hard +"linguist-js@npm:^2.5.3": + version: 2.5.4 + resolution: "linguist-js@npm:2.5.4" + dependencies: + binary-extensions: ^2.2.0 + commander: ^9.5.0 + common-path-prefix: ^3.0.0 + cross-fetch: ^3.1.5 + ignore: ^5.2.4 + isbinaryfile: ^4.0.8 <5 + js-yaml: ^4.1.0 + node-cache: ^5.1.2 + bin: + linguist: bin/index.js + linguist-js: bin/index.js + checksum: 92ebfc45f0f17a056153068f7e939a3e6755307d3696673597e824011d83b698063241aa7d082a7f9059416efce85a49056f0a31b3158c25f79e273e2f51e309 + languageName: node + linkType: hard + "linkify-it@npm:^3.0.1": version: 3.0.2 resolution: "linkify-it@npm:3.0.2" @@ -27686,8 +28225,8 @@ __metadata: linkType: hard "lint-staged@npm:^13.0.0": - version: 13.1.0 - resolution: "lint-staged@npm:13.1.0" + version: 13.1.2 + resolution: "lint-staged@npm:13.1.2" dependencies: cli-truncate: ^3.1.0 colorette: ^2.0.19 @@ -27704,7 +28243,7 @@ __metadata: yaml: ^2.1.3 bin: lint-staged: bin/lint-staged.js - checksum: adf20c4ca9285c4a93b06598b970d71b04cfe58a1a4c9006f753b83e02c1c622d1866c32a4f1e7e29a98091c501eac3345f7678af247b4f97d5be88b3d8727c1 + checksum: f854ad5c88542b8f06e27f3b4046927a4f3d4a451a04e079526559d819a325762268f65bd2df7156bcc0cb5f531f621c42cdb824b403f537c78305adc9e56a54 languageName: node linkType: hard @@ -27771,22 +28310,6 @@ __metadata: languageName: node linkType: hard -"load-bmfont@npm:^1.3.1, load-bmfont@npm:^1.4.0": - version: 1.4.1 - resolution: "load-bmfont@npm:1.4.1" - dependencies: - buffer-equal: 0.0.1 - mime: ^1.3.4 - parse-bmfont-ascii: ^1.0.3 - parse-bmfont-binary: ^1.0.5 - parse-bmfont-xml: ^1.1.4 - phin: ^2.9.1 - xhr: ^2.0.1 - xtend: ^4.0.0 - checksum: 688d932fb0dc4c9333747736ccd926261f0b91734b7bdb6ff24f8659ef068a0f0b2278084b208851afac0beec79af7bd6664fe2ed5b6c5e1db88755fc25f785e - languageName: node - linkType: hard - "load-json-file@npm:^1.0.0": version: 1.1.0 resolution: "load-json-file@npm:1.1.0" @@ -27800,18 +28323,6 @@ __metadata: languageName: node linkType: hard -"load-json-file@npm:^4.0.0": - version: 4.0.0 - resolution: "load-json-file@npm:4.0.0" - dependencies: - graceful-fs: ^4.1.2 - parse-json: ^4.0.0 - pify: ^3.0.0 - strip-bom: ^3.0.0 - checksum: 8f5d6d93ba64a9620445ee9bde4d98b1eac32cf6c8c2d20d44abfa41a6945e7969456ab5f1ca2fb06ee32e206c9769a20eec7002fe290de462e8c884b6b8b356 - languageName: node - linkType: hard - "load-yaml-file@npm:^0.2.0": version: 0.2.0 resolution: "load-yaml-file@npm:0.2.0" @@ -27860,16 +28371,6 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^2.0.0": - version: 2.0.0 - resolution: "locate-path@npm:2.0.0" - dependencies: - p-locate: ^2.0.0 - path-exists: ^3.0.0 - checksum: 02d581edbbbb0fa292e28d96b7de36b5b62c2fa8b5a7e82638ebb33afa74284acf022d3b1e9ae10e3ffb7658fbc49163fcd5e76e7d1baaa7801c3e05a81da755 - languageName: node - linkType: hard - "locate-path@npm:^3.0.0": version: 3.0.0 resolution: "locate-path@npm:3.0.0" @@ -28101,7 +28602,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4.17.21, lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.13, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:^4.7.0, lodash@npm:~4.17.0, lodash@npm:~4.17.15": +"lodash@npm:4.17.21, lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:^4.7.0, lodash@npm:~4.17.0, lodash@npm:~4.17.15": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -28131,15 +28632,16 @@ __metadata: linkType: hard "logform@npm:^2.3.2, logform@npm:^2.4.0": - version: 2.4.2 - resolution: "logform@npm:2.4.2" + version: 2.5.1 + resolution: "logform@npm:2.5.1" dependencies: "@colors/colors": 1.5.0 + "@types/triple-beam": ^1.3.2 fecha: ^4.2.0 ms: ^2.1.1 safe-stable-stringify: ^2.3.1 triple-beam: ^1.3.0 - checksum: 3d00f4e1ccaf0a86886aabbf66d1f1d207441d5b408f103457da6d64d055aee76c02af4b40a31ca77a1db4cbcdecb007149f731536c39cbd89b7b6ba3dda6d7b + checksum: 08fdf03be5bb69af33bac214eb4f6a0c83ad3821a30de498925fccb61e993e5a4a87470aab356ca2110c11e4643685bed5597ca5f46dd1cd11437c44a0e0e3c2 languageName: node linkType: hard @@ -28259,6 +28761,13 @@ __metadata: languageName: node linkType: hard +"luxon@npm:^2.0.2": + version: 2.5.2 + resolution: "luxon@npm:2.5.2" + checksum: d8b671ffd2ff0b438af862ac11082a81b3aedd9f8b6a4ca636f944224f8dd381125cd4d274ca0a8aedcaf2cfca0fc2ca96fe4cc1b16a28b7af701afb95c0bff8 + languageName: node + linkType: hard + "luxon@npm:^3.0.0, luxon@npm:^3.2.1": version: 3.2.1 resolution: "luxon@npm:3.2.1" @@ -28658,7 +29167,7 @@ __metadata: languageName: node linkType: hard -"mdurl@npm:^1.0.0, mdurl@npm:^1.0.1": +"mdurl@npm:^1.0.0, mdurl@npm:^1.0.1, mdurl@npm:~1.0.1": version: 1.0.1 resolution: "mdurl@npm:1.0.1" checksum: 71731ecba943926bfbf9f9b51e28b5945f9411c4eda80894221b47cc105afa43ba2da820732b436f0798fd3edbbffcd1fc1415843c41a87fea08a41cc1e3d02b @@ -28770,13 +29279,6 @@ __metadata: languageName: node linkType: hard -"merge@npm:^1.2.1": - version: 1.2.1 - resolution: "merge@npm:1.2.1" - checksum: 2298c4fdcf64561f320b92338681f7ffcafafb579a6e294066ae3e7bd10ae25df363903d2f028072733b9f79a1f75d2b999aef98ad5d73de13641da39cda0913 - languageName: node - linkType: hard - "meros@npm:1.2.1, meros@npm:^1.1.4": version: 1.2.1 resolution: "meros@npm:1.2.1" @@ -28789,6 +29291,13 @@ __metadata: languageName: node linkType: hard +"meta-png@npm:1.0.5": + version: 1.0.5 + resolution: "meta-png@npm:1.0.5" + checksum: b8c92a696d5e78ef7f0ab53eb9039f108ee0c55cdac9d70e92e5fe2187b090176c0489426d58e594c7d1096481dac37913dd1265f28dfa39926db4d702a5e21d + languageName: node + linkType: hard + "methods@npm:^1.0.0, methods@npm:^1.1.2, methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -29178,7 +29687,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:1.6.0, mime@npm:^1.3.4": +"mime@npm:1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" bin: @@ -29240,15 +29749,6 @@ __metadata: languageName: node linkType: hard -"min-document@npm:^2.19.0": - version: 2.19.0 - resolution: "min-document@npm:2.19.0" - dependencies: - dom-walk: ^0.1.0 - checksum: da6437562ea2228041542a2384528e74e22d1daa1a4ec439c165abf0b9d8a63e17e3b8a6dc6e0c731845e85301198730426932a0e813d23f932ca668340c9623 - languageName: node - linkType: hard - "min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -29319,10 +29819,10 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6": - version: 1.2.7 - resolution: "minimist@npm:1.2.7" - checksum: 7346574a1038ca23c32e02252f603801f09384dd1d78b69a943a4e8c2c28730b80e96193882d3d3b22a063445f460e48316b29b8a25addca2d7e5e8f75478bec +"minimist@npm:>=1.2.2, minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.7": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0 languageName: node linkType: hard @@ -29411,7 +29911,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3, minipass@npm:^3.1.6, minipass@npm:^3.3.5": +"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3, minipass@npm:^3.1.6": version: 3.3.5 resolution: "minipass@npm:3.3.5" dependencies: @@ -29464,24 +29964,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:0.3.0": - version: 0.3.0 - resolution: "mkdirp@npm:0.3.0" - checksum: 3ec9cda8bd89b64892728e5092bc79e88382e444d4bbde040c2fb8d7034dc70682cfdd729e93241fd5243d2397324c420ef68c717d806db51bf96c0fc80f4b1d - languageName: node - linkType: hard - -"mkdirp@npm:^0.5.1": - version: 0.5.5 - resolution: "mkdirp@npm:0.5.5" - dependencies: - minimist: ^1.2.5 - bin: - mkdirp: bin/cmd.js - checksum: 3bce20ea525f9477befe458ab85284b0b66c8dc3812f94155af07c827175948cdd8114852ac6c6d82009b13c1048c37f6d98743eb019651ee25c39acc8aabe7d - languageName: node - linkType: hard - "mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -29518,6 +30000,15 @@ __metadata: languageName: node linkType: hard +"move-file@npm:2.1.0": + version: 2.1.0 + resolution: "move-file@npm:2.1.0" + dependencies: + path-exists: ^4.0.0 + checksum: 2b92bbe047a302b593fcb2c0bf1131bb090ec80b3264569fc80d782c8ff829eecc13573943fa4d804c9747dec612ef2d1e84a5cfcf29cbc64a69ffcbb7ea09b3 + languageName: node + linkType: hard + "mri@npm:1.1.4": version: 1.1.4 resolution: "mri@npm:1.1.4" @@ -29553,38 +30044,9 @@ __metadata: languageName: node linkType: hard -"msw@npm:^0.39.2": - version: 0.39.2 - resolution: "msw@npm:0.39.2" - dependencies: - "@mswjs/cookies": ^0.2.0 - "@mswjs/interceptors": ^0.15.1 - "@open-draft/until": ^1.0.3 - "@types/cookie": ^0.4.1 - "@types/js-levenshtein": ^1.1.1 - chalk: 4.1.1 - chokidar: ^3.4.2 - cookie: ^0.4.2 - graphql: ^16.3.0 - headers-polyfill: ^3.0.4 - inquirer: ^8.2.0 - is-node-process: ^1.0.1 - js-levenshtein: ^1.1.6 - node-fetch: ^2.6.7 - path-to-regexp: ^6.2.0 - statuses: ^2.0.0 - strict-event-emitter: ^0.2.0 - type-fest: ^1.2.2 - yargs: ^17.3.1 - bin: - msw: cli/index.js - checksum: 4802f5568cbaadedd488f03b953523fb5dd7e1b8e48a85f142d7cfd1b8c25241729a0af4a06b9f2be543c18b67475c8777fa4924bdc6f1de19dbe142ea4a8405 - languageName: node - linkType: hard - -"msw@npm:^0.49.0": - version: 0.49.2 - resolution: "msw@npm:0.49.2" +"msw@npm:^1.0.0, msw@npm:^1.0.1": + version: 1.0.1 + resolution: "msw@npm:1.0.1" dependencies: "@mswjs/cookies": ^0.2.2 "@mswjs/interceptors": ^0.17.5 @@ -29602,7 +30064,7 @@ __metadata: node-fetch: ^2.6.7 outvariant: ^1.3.0 path-to-regexp: ^6.2.0 - strict-event-emitter: ^0.2.6 + strict-event-emitter: ^0.4.3 type-fest: ^2.19.0 yargs: ^17.3.1 peerDependencies: @@ -29612,7 +30074,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10ec35671e10e53c2a24ca22c5e7a2262d4cdfb409a403a3ffe221150af404805490e7661611a581986e61790bb8996ce8a0f057d521ef359498304c2431234a + checksum: e5d6cbe67067ca46f42aaa33011f53a4e5a873abcf81c52a6996536b3c41a88f522821d3a4cf589658252581506afddb58fd8c3dfd79a5cbf06e26e14f9570b6 languageName: node linkType: hard @@ -29824,6 +30286,24 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^3.2.1": + version: 3.2.1 + resolution: "node-addon-api@npm:3.2.1" + dependencies: + node-gyp: latest + checksum: 2369986bb0881ccd9ef6bacdf39550e07e089a9c8ede1cbc5fc7712d8e2faa4d50da0e487e333d4125f8c7a616c730131d1091676c9d499af1d74560756b4a18 + languageName: node + linkType: hard + +"node-addon-api@npm:^5.0.0": + version: 5.1.0 + resolution: "node-addon-api@npm:5.1.0" + dependencies: + node-gyp: latest + checksum: 2508bd2d2981945406243a7bd31362fc7af8b70b8b4d65f869c61731800058fb818cc2fd36c8eac714ddd0e568cc85becf5e165cebbdf7b5024d5151bbc75ea1 + languageName: node + linkType: hard + "node-cache@npm:^5.1.2": version: 5.1.2 resolution: "node-cache@npm:5.1.2" @@ -29849,7 +30329,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:2.6.7, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7": +"node-fetch@npm:2.6.7": version: 2.6.7 resolution: "node-fetch@npm:2.6.7" dependencies: @@ -29863,6 +30343,30 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^1.0.1": + version: 1.7.3 + resolution: "node-fetch@npm:1.7.3" + dependencies: + encoding: ^0.1.11 + is-stream: ^1.0.1 + checksum: 3bb0528c05d541316ebe52770d71ee25a6dce334df4231fd55df41a644143e07f068637488c18a5b0c43f05041dbd3346752f9e19b50df50569a802484544d5b + languageName: node + linkType: hard + +"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7": + version: 2.6.9 + resolution: "node-fetch@npm:2.6.9" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: acb04f9ce7224965b2b59e71b33c639794d8991efd73855b0b250921382b38331ffc9d61bce502571f6cc6e11a8905ca9b1b6d4aeb586ab093e2756a1fd190d0 + languageName: node + linkType: hard + "node-forge@npm:^1, node-forge@npm:^1.3.1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" @@ -29870,6 +30374,17 @@ __metadata: languageName: node linkType: hard +"node-gyp-build@npm:^4.3.0": + version: 4.6.0 + resolution: "node-gyp-build@npm:4.6.0" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 25d78c5ef1f8c24291f4a370c47ba52fcea14f39272041a90a7894cd50d766f7c8cb8fb06c0f42bf6f69b204b49d9be3c8fc344aac09714d5bdb95965499eb15 + languageName: node + linkType: hard + "node-gyp@npm:^8.2.0": version: 8.4.1 resolution: "node-gyp@npm:8.4.1" @@ -29995,17 +30510,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:1.0.10, nopt@npm:~1.0.10": - version: 1.0.10 - resolution: "nopt@npm:1.0.10" - dependencies: - abbrev: 1 - bin: - nopt: ./bin/nopt.js - checksum: f62575aceaa3be43f365bf37a596b89bbac2e796b001b6d2e2a85c2140a4e378ff919e2753ccba959c4fd344776fc88c29b393bc167fa939fb1513f126f4cd45 - languageName: node - linkType: hard - "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0" @@ -30028,6 +30532,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:~1.0.10": + version: 1.0.10 + resolution: "nopt@npm:1.0.10" + dependencies: + abbrev: 1 + bin: + nopt: ./bin/nopt.js + checksum: f62575aceaa3be43f365bf37a596b89bbac2e796b001b6d2e2a85c2140a4e378ff919e2753ccba959c4fd344776fc88c29b393bc167fa939fb1513f126f4cd45 + languageName: node + linkType: hard + "normalize-package-data@npm:^2.3.2, normalize-package-data@npm:^2.5.0": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -30056,6 +30571,13 @@ __metadata: languageName: node linkType: hard +"normalize-scroll-left@npm:^0.1.2": + version: 0.1.2 + resolution: "normalize-scroll-left@npm:0.1.2" + checksum: 817d5a659ba6f14f458cd03a5a0f98ec2a9d7e6c63c160f2db164827b87bb77c0237752890a18d11054fe628907493c5f4d8914907585ec9fbba3de26d2a6815 + languageName: node + linkType: hard + "normalize-url@npm:^6.0.1": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -30187,7 +30709,7 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^4.0.1, npmlog@npm:^4.1.2": +"npmlog@npm:^4.1.2": version: 4.1.2 resolution: "npmlog@npm:4.1.2" dependencies: @@ -30374,7 +30896,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.5, object.values@npm:^1.1.6": +"object.values@npm:^1.1.6": version: 1.1.6 resolution: "object.values@npm:1.1.6" dependencies: @@ -30424,13 +30946,6 @@ __metadata: languageName: node linkType: hard -"omggif@npm:^1.0.9": - version: 1.0.10 - resolution: "omggif@npm:1.0.10" - checksum: 15102e46b6fa0fba32d7e948f702623cdc3cdcdfd64b2d33c6e29a61f366ffd0f250da55d66f5217dce5b93ba9c67763fa998652791a5c7f2201a3bde2c4db45 - languageName: node - linkType: hard - "on-finished@npm:2.4.1, on-finished@npm:^2.3.0": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -30531,14 +31046,14 @@ __metadata: linkType: hard "openid-client@npm:^5.2.1, openid-client@npm:^5.3.0": - version: 5.3.1 - resolution: "openid-client@npm:5.3.1" + version: 5.4.0 + resolution: "openid-client@npm:5.4.0" dependencies: jose: ^4.10.0 lru-cache: ^6.0.0 object-hash: ^2.0.1 oidc-token-hash: ^5.0.1 - checksum: e8993cfe6ac55942fb7a5fc4db8ab88ab36bbcf2a6519993a9a2e288bfc248fcfb07804b6785eb7852a9eea0eec27f80ae57a29193c0b835f31f611b11b9bda1 + checksum: d198dbcaf0aa1d8b9f0f1d99ae30e90ec594eaf9ae668b5d61f2ee9af94a4460968d8dea7e53bd18903a1fb9c445e05aefca965bb88be3688a4bac273d1634d9 languageName: node linkType: hard @@ -30670,15 +31185,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^1.1.0": - version: 1.3.0 - resolution: "p-limit@npm:1.3.0" - dependencies: - p-try: ^1.0.0 - checksum: 281c1c0b8c82e1ac9f81acd72a2e35d402bf572e09721ce5520164e9de07d8274451378a3470707179ad13240535558f4b277f02405ad752e08c7d5b0d54fbfd - languageName: node - linkType: hard - "p-limit@npm:^2.0.0, p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -30697,15 +31203,6 @@ __metadata: languageName: node linkType: hard -"p-locate@npm:^2.0.0": - version: 2.0.0 - resolution: "p-locate@npm:2.0.0" - dependencies: - p-limit: ^1.1.0 - checksum: e2dceb9b49b96d5513d90f715780f6f4972f46987dc32a0e18bc6c3fc74a1a5d73ec5f81b1398af5e58b99ea1ad03fd41e9181c01fa81b4af2833958696e3081 - languageName: node - linkType: hard - "p-locate@npm:^3.0.0": version: 3.0.0 resolution: "p-locate@npm:3.0.0" @@ -30749,6 +31246,15 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^5.5.0": + version: 5.5.0 + resolution: "p-map@npm:5.5.0" + dependencies: + aggregate-error: ^4.0.0 + checksum: 065cb6fca6b78afbd070dd9224ff160dc23eea96e57863c09a0c8ea7ce921043f76854be7ee0abc295cff1ac9adcf700e79a1fbe3b80b625081087be58e7effb + languageName: node + linkType: hard + "p-queue@npm:^6.6.2": version: 6.6.2 resolution: "p-queue@npm:6.6.2" @@ -30788,13 +31294,6 @@ __metadata: languageName: node linkType: hard -"p-try@npm:^1.0.0": - version: 1.0.0 - resolution: "p-try@npm:1.0.0" - checksum: 3b5303f77eb7722144154288bfd96f799f8ff3e2b2b39330efe38db5dd359e4fb27012464cd85cb0a76e9b7edd1b443568cb3192c22e7cffc34989df0bafd605 - languageName: node - linkType: hard - "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -30838,7 +31337,7 @@ __metadata: languageName: node linkType: hard -"pako@npm:^1.0.10, pako@npm:^1.0.5, pako@npm:~1.0.5": +"pako@npm:^1.0.10, pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" checksum: 1be2bfa1f807608c7538afa15d6f25baa523c30ec870a3228a89579e474a4d992f4293859524e46d5d87fd30fa17c5edf34dbef0671251d9749820b488660b16 @@ -30885,30 +31384,6 @@ __metadata: languageName: node linkType: hard -"parse-bmfont-ascii@npm:^1.0.3": - version: 1.0.6 - resolution: "parse-bmfont-ascii@npm:1.0.6" - checksum: de3f6671f183c3e9d64bb4812b0407693b5fd0d24e9d16b2e106bb9eef809d64a6cc061f39ca29bb10c5c2e47e241e91b7aeefa587391fff7ccb27ab9db5012e - languageName: node - linkType: hard - -"parse-bmfont-binary@npm:^1.0.5": - version: 1.0.6 - resolution: "parse-bmfont-binary@npm:1.0.6" - checksum: ca37fb1e92f5941fddc5342b45857fafd27f00d2bd5fa44dd504bec6faeab97536c95ad45260c2dd5fc4c63de71e525663d3cdac09d038cbca803d97c669add5 - languageName: node - linkType: hard - -"parse-bmfont-xml@npm:^1.1.4": - version: 1.1.4 - resolution: "parse-bmfont-xml@npm:1.1.4" - dependencies: - xml-parse-from-string: ^1.0.0 - xml2js: ^0.4.5 - checksum: 879e5435be44f22b8c4934e2e1d2754a6d90a9ddb16309360daff965e1428d877b673f3d1fafaab4fef437c912a0db9f85545e0dd375ec62df7d4d328450d257 - languageName: node - linkType: hard - "parse-conflict-json@npm:^2.0.1": version: 2.0.1 resolution: "parse-conflict-json@npm:2.0.1" @@ -30959,13 +31434,6 @@ __metadata: languageName: node linkType: hard -"parse-headers@npm:^2.0.0": - version: 2.0.4 - resolution: "parse-headers@npm:2.0.4" - checksum: 29519ac013e100c11a67d0fc64eb33ae86523abf547f71dba36d484dcd16a2835dd11f31303f4ded27c40133dca5a5fe4d15b77f49091e470a6f74a023c59c4a - languageName: node - linkType: hard - "parse-json@npm:^2.2.0": version: 2.2.0 resolution: "parse-json@npm:2.2.0" @@ -30975,16 +31443,6 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^4.0.0": - version: 4.0.0 - resolution: "parse-json@npm:4.0.0" - dependencies: - error-ex: ^1.3.1 - json-parse-better-errors: ^1.0.1 - checksum: 0fe227d410a61090c247e34fa210552b834613c006c2c64d9a05cfe9e89cf8b4246d1246b1a99524b53b313e9ac024438d0680f67e33eaed7e6f38db64cfe7b5 - languageName: node - linkType: hard - "parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" @@ -30997,13 +31455,6 @@ __metadata: languageName: node linkType: hard -"parse-package-name@npm:^0.1.0": - version: 0.1.0 - resolution: "parse-package-name@npm:0.1.0" - checksum: 7d69b1deeaf82d5a59536c5a8772c8db003b1415dd2465073343dfab5caf90917ecfffdd71d1c004f5cf963c3881ccb8899d0ff820eac705a272e817e246de14 - languageName: node - linkType: hard - "parse-path@npm:^7.0.0": version: 7.0.0 resolution: "parse-path@npm:7.0.0" @@ -31038,20 +31489,6 @@ __metadata: languageName: node linkType: hard -"parseqs@npm:0.0.6": - version: 0.0.6 - resolution: "parseqs@npm:0.0.6" - checksum: 7fc4ff4ba59764060bb8529875f6d4313056ea6939ff579b22dd7bd6f6033035e1fd2d6a559ab48ef0a7fa29a9d7731c982bfd1594e9115141fe1c328485ce9e - languageName: node - linkType: hard - -"parseuri@npm:0.0.6": - version: 0.0.6 - resolution: "parseuri@npm:0.0.6" - checksum: fa430e40f0c75293a28e5f1023da5f51a5038d5e34c48c517b0d5187143f6bcc67d3091a062b68765db4a22757e488c7d15854f9d1921f2c2b9afa5ca0629a84 - languageName: node - linkType: hard - "parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -31290,7 +31727,7 @@ __metadata: languageName: node linkType: hard -"path-is-inside@npm:1.0.2, path-is-inside@npm:^1.0.2": +"path-is-inside@npm:1.0.2": version: 1.0.2 resolution: "path-is-inside@npm:1.0.2" checksum: 0b5b6c92d3018b82afb1f74fe6de6338c4c654de4a96123cb343f2b747d5606590ac0c890f956ed38220a4ab59baddfd7b713d78a62d240b20b14ab801fa02cb @@ -31389,15 +31826,6 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^3.0.0": - version: 3.0.0 - resolution: "path-type@npm:3.0.0" - dependencies: - pify: ^3.0.0 - checksum: 735b35e256bad181f38fa021033b1c33cfbe62ead42bb2222b56c210e42938eecb272ae1949f3b6db4ac39597a61b44edd8384623ec4d79bfdc9a9c0f12537a6 - languageName: node - linkType: hard - "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -31542,10 +31970,10 @@ __metadata: languageName: node linkType: hard -"phin@npm:^2.9.1": - version: 2.9.3 - resolution: "phin@npm:2.9.3" - checksum: 7e2abd7be74a54eb7be92dccb1d7a019725c8adaa79ac22a38f25220f9a859393e654ea753a559d326aed7bbc966fadac88270cc8c39d78896f7784219560c47 +"photoswipe@npm:^5.3.5": + version: 5.3.6 + resolution: "photoswipe@npm:5.3.6" + checksum: 192004e33dbba7567744c96835b43f17a118653a24e099f9e31c2ddea9d7214ac7c28ff8994d4f7c20720c7f7fd00b62ad8ade5e3d59994f00a27390419ba11c languageName: node linkType: hard @@ -31579,13 +32007,6 @@ __metadata: languageName: node linkType: hard -"pify@npm:^3.0.0": - version: 3.0.0 - resolution: "pify@npm:3.0.0" - checksum: 6cdcbc3567d5c412450c53261a3f10991665d660961e06605decf4544a61a97a54fefe70a68d5c37080ff9d6f4cf51444c90198d1ba9f9309a6c0d6e9f5c4fde - languageName: node - linkType: hard - "pify@npm:^4.0.1": version: 4.0.1 resolution: "pify@npm:4.0.1" @@ -31665,14 +32086,14 @@ __metadata: languageName: node linkType: hard -"pixelmatch@npm:^4.0.2": - version: 4.0.2 - resolution: "pixelmatch@npm:4.0.2" +"pixelmatch@npm:5.3.0": + version: 5.3.0 + resolution: "pixelmatch@npm:5.3.0" dependencies: - pngjs: ^3.0.0 + pngjs: ^6.0.0 bin: pixelmatch: bin/pixelmatch - checksum: 9c5c1329001938cae6d01e2bb84a909ba767f8256bcafc075422cea2a4dbaa8bebd44fceaa4b4ce7cdc36d11f20d4f1ba0cf669851d5649b32d8d1d27e4f5a36 + checksum: f542713d89536551181ad9ddb666a1792ba00a8632d831093232a075cb3ccac05856e7a453ed7d0a41aaef64dcb5962e8ae5cbe646dd2761790d8ee51b0a0743 languageName: node linkType: hard @@ -31724,10 +32145,10 @@ __metadata: languageName: node linkType: hard -"pngjs@npm:^3.0.0, pngjs@npm:^3.3.3": - version: 3.4.0 - resolution: "pngjs@npm:3.4.0" - checksum: 8bd40bd698abd16b72c97b85cb858c80894fbedc76277ce72a784aa441e14795d45d9856e97333ca469b34b67528860ffc8a7317ca6beea349b645366df00bcd +"pngjs@npm:6.0.0, pngjs@npm:^6.0.0": + version: 6.0.0 + resolution: "pngjs@npm:6.0.0" + checksum: ab6c285086060087097eab9fe6b5a528a24f9e79c03dea2b4fd6264ed4fdb5beff4a3257eeeaf2a9dc18249b539609c2a4e4013c567164a1f6b5ba2c974d5ecb languageName: node linkType: hard @@ -31738,6 +32159,13 @@ __metadata: languageName: node linkType: hard +"popper.js@npm:^1.14.1": + version: 1.16.1 + resolution: "popper.js@npm:1.16.1" + checksum: c56ae5001ec50a77ee297a8061a0221d99d25c7348d2e6bcd3e45a0d0f32a1fd81bca29d46cb0d4bdf13efb77685bd6a0ce93f9eb3c608311a461f945fffedbe + languageName: node + linkType: hard + "postcss-calc@npm:^8.0.0": version: 8.0.0 resolution: "postcss-calc@npm:8.0.0" @@ -32147,13 +32575,13 @@ __metadata: linkType: hard "postcss@npm:^8.1.0, postcss@npm:^8.4.19": - version: 8.4.20 - resolution: "postcss@npm:8.4.20" + version: 8.4.21 + resolution: "postcss@npm:8.4.21" dependencies: nanoid: ^3.3.4 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: 1a5609ea1c1b204f9c2974a0019ae9eef2d99bf645c2c9aac675166c4cb1005be7b5e2ba196160bc771f5d9ac896ed883f236f888c891e835e59d28fff6651aa + checksum: e39ac60ccd1542d4f9d93d894048aac0d686b3bb38e927d8386005718e6793dbbb46930f0a523fe382f1bbd843c6d980aaea791252bf5e176180e5a4336d9679 languageName: node linkType: hard @@ -32187,9 +32615,9 @@ __metadata: languageName: node linkType: hard -"prebuild-install@npm:^7.1.0": - version: 7.1.0 - resolution: "prebuild-install@npm:7.1.0" +"prebuild-install@npm:^7.1.0, prebuild-install@npm:^7.1.1": + version: 7.1.1 + resolution: "prebuild-install@npm:7.1.1" dependencies: detect-libc: ^2.0.0 expand-template: ^2.0.3 @@ -32198,7 +32626,6 @@ __metadata: mkdirp-classic: ^0.5.3 napi-build-utils: ^1.0.1 node-abi: ^3.3.0 - npmlog: ^4.0.1 pump: ^3.0.0 rc: ^1.2.7 simple-get: ^4.0.0 @@ -32206,7 +32633,7 @@ __metadata: tunnel-agent: ^0.6.0 bin: prebuild-install: bin.js - checksum: 204f2d89c6d6179fa1039036514aa72f7d0b537e421ef72c40840286e318f41489f00f22c6acc725cce6e10d43825b69dcabeaadfc917db781c58cd56fc25f90 + checksum: dbf96d0146b6b5827fc8f67f72074d2e19c69628b9a7a0a17d0fad1bf37e9f06922896972e074197fc00a52eae912993e6ef5a0d471652f561df5cb516f3f467 languageName: node linkType: hard @@ -32243,21 +32670,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^1.16.4": - version: 1.19.1 - resolution: "prettier@npm:1.19.1" - bin: - prettier: ./bin-prettier.js - checksum: bc78219e0f8173a808f4c6c8e0a137dd8ebd4fbe013e63fe1a37a82b48612f17b8ae8e18a992adf802ee2cf7428f14f084e7c2846ca5759cf4013c6e54810e1f - languageName: node - linkType: hard - "prettier@npm:^2.2.1, prettier@npm:^2.7.1": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" + version: 2.8.4 + resolution: "prettier@npm:2.8.4" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e languageName: node linkType: hard @@ -32422,7 +32840,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -32474,16 +32892,16 @@ __metadata: languageName: node linkType: hard -"protobufjs-cli@npm:1.0.2": - version: 1.0.2 - resolution: "protobufjs-cli@npm:1.0.2" +"protobufjs-cli@npm:1.1.1": + version: 1.1.1 + resolution: "protobufjs-cli@npm:1.1.1" dependencies: chalk: ^4.0.0 escodegen: ^1.13.0 espree: ^9.0.0 estraverse: ^5.1.0 glob: ^8.0.0 - jsdoc: ^3.6.3 + jsdoc: ^4.0.0 minimist: ^1.2.0 semver: ^7.1.2 tmp: ^0.2.1 @@ -32493,13 +32911,13 @@ __metadata: bin: pbjs: bin/pbjs pbts: bin/pbts - checksum: 75dfa8bb76ea390c4f4926120439892fce6c730ec56960e85d5f03cac9c390fd7467d1254833542d722616ab4cb64a622e6de2fb7c75e7c42972878ae447b773 + checksum: 124a2cb10d6fccdd6e8f2984b0f7d9a351d9c1efd17f237acd4a9e7c4b82d63265364b1c86bfa5c6a6fa17d7119182c4c323a8972c0078e1ac5c5f653d096f9b languageName: node linkType: hard -"protobufjs@npm:7.1.2, protobufjs@npm:^7.0.0": - version: 7.1.2 - resolution: "protobufjs@npm:7.1.2" +"protobufjs@npm:7.2.2, protobufjs@npm:^7.0.0": + version: 7.2.2 + resolution: "protobufjs@npm:7.2.2" dependencies: "@protobufjs/aspromise": ^1.1.2 "@protobufjs/base64": ^1.1.2 @@ -32513,7 +32931,7 @@ __metadata: "@protobufjs/utf8": ^1.1.0 "@types/node": ">=13.7.0" long: ^5.0.0 - checksum: ae41669b1b0372fb1d49f506f2d1f2b0fb3dc3cece85987b17bcb544e4cef7c8d27f480486cdec324146ad0a5d22a327166a7ea864a9b3e49cc3c92a5d3f6500 + checksum: 86166e8f3e46789fa4d117ae72c17356db36bf87c0e0710d224be32e543b1c378a94e66dc2b1de5af45edfc25f56acfc7e688c50c956426a3ae97bc474f4445c languageName: node linkType: hard @@ -32818,7 +33236,7 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:2.5.1, raw-body@npm:^2.4.1": +"raw-body@npm:2.5.1": version: 2.5.1 resolution: "raw-body@npm:2.5.1" dependencies: @@ -32830,6 +33248,18 @@ __metadata: languageName: node linkType: hard +"raw-body@npm:^2.4.1": + version: 2.5.2 + resolution: "raw-body@npm:2.5.2" + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + checksum: ba1583c8d8a48e8fbb7a873fdbb2df66ea4ff83775421bfe21ee120140949ab048200668c47d9ae3880012f6e217052690628cf679ddfbd82c9fc9358d574676 + languageName: node + linkType: hard + "rc-progress@npm:3.4.1": version: 3.4.1 resolution: "rc-progress@npm:3.4.1" @@ -33000,6 +33430,19 @@ __metadata: languageName: node linkType: hard +"react-event-listener@npm:^0.6.2": + version: 0.6.6 + resolution: "react-event-listener@npm:0.6.6" + dependencies: + "@babel/runtime": ^7.2.0 + prop-types: ^15.6.0 + warning: ^4.0.1 + peerDependencies: + react: ^16.3.0 + checksum: 0287e0ae8cbf0a4c03889ffc2b745f5494b3edea0f4667357d709f5646dd78c7289ce305b6f473ec6ac5789f8a937c86b7e543d0249f0bf787bccdb1eab7ecd0 + languageName: node + linkType: hard + "react-fast-compare@npm:^3.1.1": version: 3.2.0 resolution: "react-fast-compare@npm:3.2.0" @@ -33040,15 +33483,6 @@ __metadata: languageName: node linkType: hard -"react-icons@npm:^4.4.0": - version: 4.4.0 - resolution: "react-icons@npm:4.4.0" - peerDependencies: - react: "*" - checksum: dd93a1dcc8ac8a3cf46a2b33e80b586fa967331fa5fcb90d061abf276155a8363a331643e203d67cb9bab4261d00a757da854bcce1fce2c6e4258bf3e33f711b - languageName: node - linkType: hard - "react-immutable-proptypes@npm:2.2.0": version: 2.2.0 resolution: "react-immutable-proptypes@npm:2.2.0" @@ -33080,7 +33514,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.10.2, react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.8.0, react-is@npm:^16.8.6, react-is@npm:^16.9.0": +"react-is@npm:^16.10.2, react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.6.3, react-is@npm:^16.7.0, react-is@npm:^16.8.0, react-is@npm:^16.8.6, react-is@npm:^16.9.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f @@ -33101,7 +33535,7 @@ __metadata: languageName: node linkType: hard -"react-lifecycles-compat@npm:^3.0.4": +"react-lifecycles-compat@npm:^3.0.2, react-lifecycles-compat@npm:^3.0.4": version: 3.0.4 resolution: "react-lifecycles-compat@npm:3.0.4" checksum: a904b0fc0a8eeb15a148c9feb7bc17cec7ef96e71188280061fc340043fd6d8ee3ff233381f0e8f95c1cf926210b2c4a31f38182c8f35ac55057e453d6df204f @@ -33109,8 +33543,8 @@ __metadata: linkType: hard "react-markdown@npm:^8.0.0": - version: 8.0.4 - resolution: "react-markdown@npm:8.0.4" + version: 8.0.5 + resolution: "react-markdown@npm:8.0.5" dependencies: "@types/hast": ^2.0.0 "@types/prop-types": ^15.0.0 @@ -33123,14 +33557,14 @@ __metadata: remark-parse: ^10.0.0 remark-rehype: ^10.0.0 space-separated-tokens: ^2.0.0 - style-to-object: ^0.3.0 + style-to-object: ^0.4.0 unified: ^10.0.0 unist-util-visit: ^4.0.0 vfile: ^5.0.0 peerDependencies: "@types/react": ">=16" react: ">=16" - checksum: 9feec3734694ef05f450779a1adac10dbb768b8f3adbf4c81f59ca3cea8fd2f6a578fa4c488183597a7418f972715e5d84705cc76f278b55c86d622e82561bf8 + checksum: 9d11b7aba16216d590e56b4744e05d2925141bfb0f5885b3d9400ccf006cd24b79ce3b3d20af8a083a01324215b58fa4c5979e44f69d54123ff1dd5dacb0dc89 languageName: node linkType: hard @@ -33203,26 +33637,26 @@ __metadata: linkType: hard "react-router-dom-stable@npm:react-router-dom@^6.3.0, react-router-dom@npm:^6.3.0": - version: 6.6.2 - resolution: "react-router-dom@npm:6.6.2" + version: 6.8.1 + resolution: "react-router-dom@npm:6.8.1" dependencies: - "@remix-run/router": 1.2.1 - react-router: 6.6.2 + "@remix-run/router": 1.3.2 + react-router: 6.8.1 peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: e0ee89a2b3768d2eb406ecefedfa24b55428145359b4138f6d38b8a99b60a4f275418910f4cdb12c069ed0237711acced3787b0c7febfa6568d7ea9cd624ff3b + checksum: fb1a9f5c7e4c83536ee7e6493f3c13f6abda66da69f9191fb8b087ab188ecc18a3a32575813a280cbeca67bcb8b0dcb6dbcef7ed599e03f38a44d694f4abcf14 languageName: node linkType: hard -"react-router-stable@npm:react-router@^6.3.0, react-router@npm:6.6.2": - version: 6.6.2 - resolution: "react-router@npm:6.6.2" +"react-router-stable@npm:react-router@^6.3.0, react-router@npm:6.8.1": + version: 6.8.1 + resolution: "react-router@npm:6.8.1" dependencies: - "@remix-run/router": 1.2.1 + "@remix-run/router": 1.3.2 peerDependencies: react: ">=16.8" - checksum: 5960d774bb3a8f697a57ee2e82a3eb0a300b7186a62b27e02773c94f1682e59435bd876d6162a08cc8daf27cac72aaab66d348fcc4d8f59240a63a911cf0050b + checksum: 8e7cc7a516adeb6060911861af99d9b512893c15ec732cbf8574872919be4f3fe439ffa8c0876d1bcbced880d081bfeda3c01cb505b5a243565d10500ee0ac36 languageName: node linkType: hard @@ -33302,7 +33736,7 @@ __metadata: languageName: node linkType: hard -"react-transition-group@npm:2.9.0": +"react-transition-group@npm:2.9.0, react-transition-group@npm:^2.2.1": version: 2.9.0 resolution: "react-transition-group@npm:2.9.0" dependencies: @@ -33614,6 +34048,22 @@ __metadata: languageName: node linkType: hard +"recompose@npm:0.28.0 - 0.30.0": + version: 0.30.0 + resolution: "recompose@npm:0.30.0" + dependencies: + "@babel/runtime": ^7.0.0 + change-emitter: ^0.1.2 + fbjs: ^0.8.1 + hoist-non-react-statics: ^2.3.1 + react-lifecycles-compat: ^3.0.2 + symbol-observable: ^1.0.4 + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 + checksum: 18e58252336d0628b22db1e38407d32e836648e6d5c9453ba37c9f8030138b3429ee3952b053a13b60311f8b60893b207a761466bb293083542db0cf317b7a41 + languageName: node + linkType: hard + "recursive-readdir@npm:^2.2.2": version: 2.2.3 resolution: "recursive-readdir@npm:2.2.3" @@ -33725,7 +34175,7 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:^0.13.11, regenerator-runtime@npm:^0.13.3, regenerator-runtime@npm:^0.13.4": +"regenerator-runtime@npm:^0.13.11, regenerator-runtime@npm:^0.13.4": version: 0.13.11 resolution: "regenerator-runtime@npm:0.13.11" checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4 @@ -34056,7 +34506,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.0, resolve@npm:~1.22.1": +"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:~1.22.1": version: 1.22.1 resolution: "resolve@npm:1.22.1" dependencies: @@ -34101,7 +34551,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.6#~builtin<compat/resolve>, resolve@patch:resolve@^1.10.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.14.2#~builtin<compat/resolve>, resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>, resolve@patch:resolve@~1.22.1#~builtin<compat/resolve>": +"resolve@patch:resolve@^1.1.6#~builtin<compat/resolve>, resolve@patch:resolve@^1.10.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.14.2#~builtin<compat/resolve>, resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.1#~builtin<compat/resolve>, resolve@patch:resolve@~1.22.1#~builtin<compat/resolve>": version: 1.22.1 resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=07638b" dependencies: @@ -34490,12 +34940,12 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.0.0, rxjs@npm:^7.5.1, rxjs@npm:^7.5.4, rxjs@npm:^7.5.5, rxjs@npm:^7.5.6": - version: 7.6.0 - resolution: "rxjs@npm:7.6.0" +"rxjs@npm:^7.0.0, rxjs@npm:^7.2.0, rxjs@npm:^7.5.1, rxjs@npm:^7.5.5, rxjs@npm:^7.5.6, rxjs@npm:^7.8.0": + version: 7.8.0 + resolution: "rxjs@npm:7.8.0" dependencies: tslib: ^2.1.0 - checksum: b3abbbfe1ddfd06fca9314b83cbd13bcddc3320429218136f75c79a4802ac430dd13873364aac1ded54fd457f8c77df332d205a92d8a1c61656565bb718c50af + checksum: 61b4d4fd323c1043d8d6ceb91f24183b28bcf5def4f01ca111511d5c6b66755bc5578587fe714ef5d67cf4c9f2e26f4490d4e1d8cabf9bd5967687835e9866a2 languageName: node linkType: hard @@ -34563,15 +35013,6 @@ __metadata: languageName: node linkType: hard -"sanitize-filename@npm:^1.6.1": - version: 1.6.3 - resolution: "sanitize-filename@npm:1.6.3" - dependencies: - truncate-utf8-bytes: ^1.0.0 - checksum: aa733c012b7823cf65730603cf3b503c641cee6b239771d3164ca482f22d81a50e434a713938d994071db18e4202625669cc56bccc9d13d818b4c983b5f47fde - languageName: node - linkType: hard - "sax@npm:1.2.1": version: 1.2.1 resolution: "sax@npm:1.2.1" @@ -34693,6 +35134,13 @@ __metadata: languageName: node linkType: hard +"select@npm:^1.1.2": + version: 1.1.2 + resolution: "select@npm:1.1.2" + checksum: 4346151e94f226ea6131e44e68e6d837f3fdee64831b756dd657cc0b02f4cb5107f867cb34a1d1216ab7737d0bf0645d44546afb030bbd8d64e891f5e4c4814e + languageName: node + linkType: hard + "selfsigned@npm:^2.0.0, selfsigned@npm:^2.1.1": version: 2.1.1 resolution: "selfsigned@npm:2.1.1" @@ -34943,6 +35391,23 @@ __metadata: languageName: node linkType: hard +"sharp@npm:0.31.2": + version: 0.31.2 + resolution: "sharp@npm:0.31.2" + dependencies: + color: ^4.2.3 + detect-libc: ^2.0.1 + node-addon-api: ^5.0.0 + node-gyp: latest + prebuild-install: ^7.1.1 + semver: ^7.3.8 + simple-get: ^4.0.1 + tar-fs: ^2.1.1 + tunnel-agent: ^0.6.0 + checksum: 076717b7a073ea47bb522ff2931b74b6608daeb6f7ae334e4848d47fdf4d23bcb18cd49044fd5fb27ef27a1a4aa87d141894d67d1c4bb15a6e2e63cf4dbe329e + languageName: node + linkType: hard + "shebang-command@npm:^1.2.0": version: 1.2.0 resolution: "shebang-command@npm:1.2.0" @@ -35100,13 +35565,6 @@ __metadata: languageName: node linkType: hard -"slash@npm:^1.0.0": - version: 1.0.0 - resolution: "slash@npm:1.0.0" - checksum: 4b6e21b1fba6184a7e2efb1dd173f692d8a845584c1bbf9dc818ff86f5a52fc91b413008223d17cc684604ee8bb9263a420b1182027ad9762e35388434918860 - languageName: node - linkType: hard - "slash@npm:^2.0.0": version: 2.0.0 resolution: "slash@npm:2.0.0" @@ -35121,6 +35579,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^4.0.0": + version: 4.0.0 + resolution: "slash@npm:4.0.0" + checksum: da8e4af73712253acd21b7853b7e0dbba776b786e82b010a5bfc8b5051a1db38ed8aba8e1e8f400dd2c9f373be91eb1c42b66e91abb407ff42b10feece5e1d2d + languageName: node + linkType: hard + "slice-ansi@npm:^3.0.0": version: 3.0.0 resolution: "slice-ansi@npm:3.0.0" @@ -35153,6 +35618,13 @@ __metadata: languageName: node linkType: hard +"slugify@npm:^1.6.4": + version: 1.6.5 + resolution: "slugify@npm:1.6.5" + checksum: a955a1b600201030f4c1daa9bb74a17d4402a0693fc40978bbd17e44e64fd72dad3bac4037422aa8aed55b5170edd57f3f4cd8f59ba331f5cf0f10f1a7795609 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -35186,68 +35658,6 @@ __metadata: languageName: node linkType: hard -"socket.io-adapter@npm:~1.1.0": - version: 1.1.2 - resolution: "socket.io-adapter@npm:1.1.2" - checksum: 8e18df7f8c471001b65e43542c5c743b63f041781bca097b4a407dd199dd757c14c0d470992f84923f48e75729a2ac0ae634fec820f20881cadae8a495bf68e9 - languageName: node - linkType: hard - -"socket.io-client@npm:2.5.0, socket.io-client@npm:^2.2.0": - version: 2.5.0 - resolution: "socket.io-client@npm:2.5.0" - dependencies: - backo2: 1.0.2 - component-bind: 1.0.0 - component-emitter: ~1.3.0 - debug: ~3.1.0 - engine.io-client: ~3.5.0 - has-binary2: ~1.0.2 - indexof: 0.0.1 - parseqs: 0.0.6 - parseuri: 0.0.6 - socket.io-parser: ~3.3.0 - to-array: 0.1.4 - checksum: a33accaf02c4a4c47ac9b09018fa4b9fa67a648a0cc92602e520ec884520050796786eb0df78fc1a9bf51e7b3d7b50ba4ffd3bda6e44df2c253a77100124669c - languageName: node - linkType: hard - -"socket.io-parser@npm:~3.3.0": - version: 3.3.3 - resolution: "socket.io-parser@npm:3.3.3" - dependencies: - component-emitter: ~1.3.0 - debug: ~3.1.0 - isarray: 2.0.1 - checksum: 6cf464e324e4207811dbf994822c0aa18ba01d615fbed8e3f069927e0e402fdc9b3ac3d2657f18d481fb8c584b1fd40152ced15b3686982a51ec843c6a76dbc2 - languageName: node - linkType: hard - -"socket.io-parser@npm:~3.4.0": - version: 3.4.1 - resolution: "socket.io-parser@npm:3.4.1" - dependencies: - component-emitter: 1.2.1 - debug: ~4.1.0 - isarray: 2.0.1 - checksum: f8bac61298375680aceb6c72ffbb47a2d950daa05e01a3b67d9330945073ae01e35aa131dcddd7e0b354ca302c51742bebaae57a50c188d4c1fe8af9461aa38d - languageName: node - linkType: hard - -"socket.io@npm:^2.2.0": - version: 2.5.0 - resolution: "socket.io@npm:2.5.0" - dependencies: - debug: ~4.1.0 - engine.io: ~3.6.0 - has-binary2: ~1.0.2 - socket.io-adapter: ~1.1.0 - socket.io-client: 2.5.0 - socket.io-parser: ~3.4.0 - checksum: 6f20a413352cb0a8bb13063249fe425c81fb54beca1bdd71221262305cf21232e6a37fdd4a232b5695dbd4d3204281a9313829c9c373c5ba01c5bd7218edd094 - languageName: node - linkType: hard - "sockjs@npm:^0.3.24": version: 0.3.24 resolution: "sockjs@npm:0.3.24" @@ -35301,7 +35711,7 @@ __metadata: languageName: node linkType: hard -"source-list-map@npm:^2.0.1": +"source-list-map@npm:^2.0.0": version: 2.0.1 resolution: "source-list-map@npm:2.0.1" checksum: 806efc6f75e7cd31e4815e7a3aaf75a45c704871ea4075cb2eb49882c6fca28998f44fc5ac91adb6de03b2882ee6fb02f951fdc85e6a22b338c32bfe19557938 @@ -35325,7 +35735,7 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.10, source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.20": +"source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.21, source-map-support@npm:~0.5.20": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: @@ -35660,8 +36070,8 @@ __metadata: linkType: hard "start-server-and-test@npm:^1.10.11": - version: 1.15.2 - resolution: "start-server-and-test@npm:1.15.2" + version: 1.15.4 + resolution: "start-server-and-test@npm:1.15.4" dependencies: arg: ^5.0.2 bluebird: 3.7.2 @@ -35670,16 +36080,16 @@ __metadata: execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 - wait-on: 6.0.1 + wait-on: 7.0.1 bin: server-test: src/bin/start.js start-server-and-test: src/bin/start.js start-test: src/bin/start.js - checksum: 3334959d8c7fb72b9a88672f531a21a90146e8c855d2f8a4aca57b116221e3d0db4dd23a572c02bf2e08cc30266dde9f95cb7e53089223e1e1c5dc8ac44c3d20 + checksum: 0df9a4710ea45ddb1a9f719e0865faa8e26a973beff693dfe244ae2d914bfc6eed4d2db8529cdcad3d53658c4655356e5aca3520485a3ef4d9d9f320956b0e7d languageName: node linkType: hard -"statuses@npm:2.0.1, statuses@npm:^2.0.0": +"statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" checksum: 18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb @@ -35781,7 +36191,7 @@ __metadata: languageName: node linkType: hard -"strict-event-emitter@npm:^0.2.0, strict-event-emitter@npm:^0.2.4, strict-event-emitter@npm:^0.2.6": +"strict-event-emitter@npm:^0.2.4": version: 0.2.7 resolution: "strict-event-emitter@npm:0.2.7" dependencies: @@ -35790,6 +36200,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.4.3": + version: 0.4.3 + resolution: "strict-event-emitter@npm:0.4.3" + checksum: ee335aba8a43bc6749d3337d78b9f6dbe8358d34b6eb38e7075dadf7f1cae621d96588663dda22dce17e6e7c7e281ddac79ca47421da8250bb4bf53b979508a5 + languageName: node + linkType: hard + "strict-uri-encode@npm:^2.0.0": version: 2.0.0 resolution: "strict-uri-encode@npm:2.0.0" @@ -35877,6 +36294,13 @@ __metadata: languageName: node linkType: hard +"string.prototype.repeat@npm:^0.2.0": + version: 0.2.0 + resolution: "string.prototype.repeat@npm:0.2.0" + checksum: 5127ce1b5bce45258905877048ac28bae71be18f1c3421011bd3979cfbcacf9c16f6b7f42daa3b999c9d90f5da39bd7136982c8e64439f02666dd6c0778ed262 + languageName: node + linkType: hard + "string.prototype.trimend@npm:^1.0.5": version: 1.0.5 resolution: "string.prototype.trimend@npm:1.0.5" @@ -36079,12 +36503,12 @@ __metadata: languageName: node linkType: hard -"style-to-object@npm:^0.3.0": - version: 0.3.0 - resolution: "style-to-object@npm:0.3.0" +"style-to-object@npm:^0.4.0": + version: 0.4.1 + resolution: "style-to-object@npm:0.4.1" dependencies: inline-style-parser: 0.1.1 - checksum: 4d7084015207f2a606dfc10c29cb5ba569f2fe8005551df7396110dd694d6ff650f2debafa95bd5d147dfb4ca50f57868e2a7f91bf5d11ef734fe7ccbd7abf59 + checksum: 2ea213e98eed21764ae1d1dc9359231a9f2d480d6ba55344c4c15eb275f0809f1845786e66d4caf62414a5cc8f112ce9425a58d251c77224060373e0db48f8c2 languageName: node linkType: hard @@ -36224,6 +36648,13 @@ __metadata: languageName: node linkType: hard +"svg-pan-zoom@npm:^3.6.0": + version: 3.6.1 + resolution: "svg-pan-zoom@npm:3.6.1" + checksum: 500b99378d321315e1668067ef6a73af3f61f9adfab75a575b21d2d216cb446def61d60ad12265203463cca9bef56b090f00e48118fa9b7f0ffa1c691e4c1621 + languageName: node + linkType: hard + "svg-parser@npm:^2.0.4": version: 2.0.4 resolution: "svg-parser@npm:2.0.4" @@ -36334,17 +36765,17 @@ __metadata: linkType: hard "swr@npm:^2.0.0": - version: 2.0.0 - resolution: "swr@npm:2.0.0" + version: 2.0.4 + resolution: "swr@npm:2.0.4" dependencies: use-sync-external-store: ^1.2.0 peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 - checksum: 4852d8318ed022a681b40a384e675f65e211104d62075c1250b02f47cc8b739cb9138a954e2191c700e2de22b010b9bd05a113c71b193abe0c351d3c4f4646d8 + checksum: 2fd26baed9a0ad3ecf24d7a64c4929f9c3be71c942e883ab369c3929aa297a47b0512c29ed8cdfd0ce2e3f5ee3f181d80c16b2b95128a0ae58bc133b4a0ea5a8 languageName: node linkType: hard -"symbol-observable@npm:1.2.0, symbol-observable@npm:^1.0.4": +"symbol-observable@npm:1.2.0, symbol-observable@npm:^1.0.4, symbol-observable@npm:^1.1.0": version: 1.2.0 resolution: "symbol-observable@npm:1.2.0" checksum: 48ffbc22e3d75f9853b3ff2ae94a44d84f386415110aea5effc24d84c502e03a4a6b7a8f75ebaf7b585780bda34eb5d6da3121f826a6f93398429d30032971b6 @@ -36358,13 +36789,6 @@ __metadata: languageName: node linkType: hard -"taffydb@npm:2.6.2": - version: 2.6.2 - resolution: "taffydb@npm:2.6.2" - checksum: 8fea9cdff71735a40320c4beeb80cb98837076cb89614bc55ac5d67561f35ebae158cfc07a193a1099b5e32746433b2c086b0cd6d56f29aa7c7678e74968335b - languageName: node - linkType: hard - "tapable@npm:^1.0.0": version: 1.1.3 resolution: "tapable@npm:1.1.3" @@ -36469,7 +36893,7 @@ __metadata: "@backstage/plugin-techdocs-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@material-ui/core": ^4.11.0 + "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 @@ -36683,13 +37107,6 @@ __metadata: languageName: node linkType: hard -"timm@npm:^1.6.1": - version: 1.7.1 - resolution: "timm@npm:1.7.1" - checksum: c80df538ec7fae50a0e3183931b20fbe97f6f2c06907d9675eb7b9d90b3f788af7742285c730192db3b066c4ab22ebae75f8d21970c5b03f38d928d5bb2a0339 - languageName: node - linkType: hard - "timsort@npm:^0.3.0": version: 0.3.0 resolution: "timsort@npm:0.3.0" @@ -36697,6 +37114,13 @@ __metadata: languageName: node linkType: hard +"tiny-emitter@npm:^2.0.0": + version: 2.1.0 + resolution: "tiny-emitter@npm:2.1.0" + checksum: fbcfb5145751a0e3b109507a828eb6d6d4501352ab7bb33eccef46e22e9d9ad3953158870a6966a59e57ab7c3f9cfac7cab8521db4de6a5e757012f4677df2dd + languageName: node + linkType: hard + "tiny-invariant@npm:^1.0.6": version: 1.1.0 resolution: "tiny-invariant@npm:1.1.0" @@ -36718,13 +37142,6 @@ __metadata: languageName: node linkType: hard -"tinycolor2@npm:^1.4.1": - version: 1.4.2 - resolution: "tinycolor2@npm:1.4.2" - checksum: 57ed262e08815a4ab0ed933edafdbc6555a17081781766149813b44a080ecbe58b3ee281e81c0e75b42e4d41679f138cfa98eabf043f829e0683c04adb12c031 - languageName: node - linkType: hard - "title-case@npm:^3.0.3": version: 3.0.3 resolution: "title-case@npm:3.0.3" @@ -36768,13 +37185,6 @@ __metadata: languageName: node linkType: hard -"to-array@npm:0.1.4": - version: 0.1.4 - resolution: "to-array@npm:0.1.4" - checksum: 396a04df5a5e74df5c8891ebb819d575d626e2929312fec3d649b4d38203bbf7185c270cb31fcad903564743895e935f2e965770bd1c6eb137dde48c4d60788d - languageName: node - linkType: hard - "to-arraybuffer@npm:^1.0.0": version: 1.0.1 resolution: "to-arraybuffer@npm:1.0.1" @@ -36948,15 +37358,6 @@ __metadata: languageName: node linkType: hard -"truncate-utf8-bytes@npm:^1.0.0": - version: 1.0.2 - resolution: "truncate-utf8-bytes@npm:1.0.2" - dependencies: - utf8-byte-length: ^1.0.1 - checksum: ad097314709ea98444ad9c80c03aac8da805b894f37ceb5685c49ad297483afe3a5ec9572ebcaff699dda72b6cd447a2ba2a3fd10e96c2628cd16d94abeb328a - languageName: node - linkType: hard - "tryer@npm:^1.0.1": version: 1.0.1 resolution: "tryer@npm:1.0.1" @@ -37066,7 +37467,14 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:~2.4.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:~2.5.0": + version: 2.5.0 + resolution: "tslib@npm:2.5.0" + checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1 + languageName: node + linkType: hard + +"tslib@npm:~2.4.0": version: 2.4.1 resolution: "tslib@npm:2.4.1" checksum: 19480d6e0313292bd6505d4efe096a6b31c70e21cf08b5febf4da62e95c265c8f571f7b36fcc3d1a17e068032f59c269fab3459d6cd3ed6949eafecf64315fca @@ -37191,7 +37599,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^1.2.1, type-fest@npm:^1.2.2": +"type-fest@npm:^1.2.1": version: 1.4.0 resolution: "type-fest@npm:1.4.0" checksum: b011c3388665b097ae6a109a437a04d6f61d81b7357f74cbcb02246f2f5bd72b888ae33631b99871388122ba0a87f4ff1c94078e7119ff22c70e52c0ff828201 @@ -37291,10 +37699,10 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^0.7.18": - version: 0.7.28 - resolution: "ua-parser-js@npm:0.7.28" - checksum: a7da4ad54527211e878ee016c2ef64efad5c2f5a31277d36c9da93b4c89ecaa64f391ad4cf158ada76a9ad8e53004a950705ff1c2f27a52ca8bfb3f1381c39ff +"ua-parser-js@npm:^0.7.18, ua-parser-js@npm:^0.7.30": + version: 0.7.33 + resolution: "ua-parser-js@npm:0.7.33" + checksum: 1510e9ec26fcaf0d8c6ae8f1078a8230e8816f083e1b5f453ea19d06b8ef2b8a596601c92148fd41899e8b3e5f83fa69c42332bd5729b931a721040339831696 languageName: node linkType: hard @@ -37381,11 +37789,11 @@ __metadata: linkType: hard "undici@npm:^5.12.0, undici@npm:^5.8.0": - version: 5.12.0 - resolution: "undici@npm:5.12.0" + version: 5.19.1 + resolution: "undici@npm:5.19.1" dependencies: busboy: ^1.6.0 - checksum: fbc227704943c05aa3dc1630695e10309c17d0a535678594d136db107c50593248e9ace70e1ab77496a6c837bf14aa2ab3c501a7a6c45fb6277dbf0846e15ffe + checksum: 57ee94ee74d944faa41dbcb2faf4e0c90069708d3aaae860185884e51376b5d457728352a8396d69a3c9cb752b62ff99a19a664c5aacb7ee61cc488af499a01c languageName: node linkType: hard @@ -37420,15 +37828,6 @@ __metadata: languageName: node linkType: hard -"unidiff@npm:1.0.2": - version: 1.0.2 - resolution: "unidiff@npm:1.0.2" - dependencies: - diff: ^2.2.2 - checksum: 442d699d44a07f7511e2ec5b6433fb562d6ddce9eb91c6c28ff23bdda60595d9d7e8c6341937f18996ca03c198d93cc9b8fb448dde211ddeeb45bb45bff8ca1e - languageName: node - linkType: hard - "unified@npm:^10.0.0": version: 10.1.0 resolution: "unified@npm:10.1.0" @@ -37736,22 +38135,6 @@ __metadata: languageName: node linkType: hard -"utf8-byte-length@npm:^1.0.1": - version: 1.0.4 - resolution: "utf8-byte-length@npm:1.0.4" - checksum: f188ca076ec094d58e7009fcc32623c5830c7f0f3e15802bfa4fdd1e759454a481fc4ac05e0fa83b7736e77af628a9ee0e57dcc89683d688fde3811473e42143 - languageName: node - linkType: hard - -"utif@npm:^2.0.1": - version: 2.0.1 - resolution: "utif@npm:2.0.1" - dependencies: - pako: ^1.0.5 - checksum: 66b0bffc18f08834a34c44846f189b2223418e8bddfff3882e07fccea54436737f7334e27c86cbff46e247487dc92377b7a17428a7aafc5d5c733d61bed39038 - languageName: node - linkType: hard - "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -38035,6 +38418,13 @@ __metadata: languageName: node linkType: hard +"viz.js@npm:2.1.2": + version: 2.1.2 + resolution: "viz.js@npm:2.1.2" + checksum: 299e5a8519a472e0e98197c5f39c0ca6f3f6c4642b6ae007ed888fea998d82455c7ae0118d09cb888bfd2c46ebe3b03d7ce321a4eef1c822a5f5719d3a6ef3bb + languageName: node + linkType: hard + "vm-browserify@npm:^1.0.1": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" @@ -38043,14 +38433,14 @@ __metadata: linkType: hard "vm2@npm:^3.9.11": - version: 3.9.13 - resolution: "vm2@npm:3.9.13" + version: 3.9.14 + resolution: "vm2@npm:3.9.14" dependencies: acorn: ^8.7.0 acorn-walk: ^8.2.0 bin: vm2: bin/vm2 - checksum: ee82c130a9d1b45558fac4e95133ae022e69fafd4bec8aada44bb2f0aebe6ff8ae9b1176e7cbafceffec35267e009b518c121ad81a5565da06aa7a50ed3a09bc + checksum: 1ed7481e07ce8e03055101b382bfbf0d725a5c9b9bbe8bf75f71501cb43a6bd22f6a0a151975ff7cea8cad136d47e66d64f0a3248913f6d3ca3c405db12bacc0 languageName: node linkType: hard @@ -38102,18 +38492,18 @@ __metadata: languageName: node linkType: hard -"wait-on@npm:6.0.1": - version: 6.0.1 - resolution: "wait-on@npm:6.0.1" +"wait-on@npm:7.0.1": + version: 7.0.1 + resolution: "wait-on@npm:7.0.1" dependencies: - axios: ^0.25.0 - joi: ^17.6.0 + axios: ^0.27.2 + joi: ^17.7.0 lodash: ^4.17.21 - minimist: ^1.2.5 - rxjs: ^7.5.4 + minimist: ^1.2.7 + rxjs: ^7.8.0 bin: wait-on: bin/wait-on - checksum: e4d62aa4145d99fe34747ccf7506d4b4d6e60dd677c0eb18a51e316d38116ace2d194e4b22a9eb7b767b0282f39878ddcc4ae9440dcb0c005c9150668747cf5b + checksum: 1e8a17d8ee6436f71d3ab82781ce31267481fcd7bbccde49b0f8124871e6e40a1acac3401f04f775ba6203853a5813352fa131620fc139914351f3b2894d573f languageName: node linkType: hard @@ -38133,6 +38523,24 @@ __metadata: languageName: node linkType: hard +"warning@npm:^3.0.0": + version: 3.0.0 + resolution: "warning@npm:3.0.0" + dependencies: + loose-envify: ^1.0.0 + checksum: c9f99a12803aab81b29858e7dc3415bf98b41baee3a4c3acdeb680d98c47b6e17490f1087dccc54432deed5711a5ce0ebcda2b27e9b5eb054c32ae50acb4419c + languageName: node + linkType: hard + +"warning@npm:^4.0.1": + version: 4.0.3 + resolution: "warning@npm:4.0.3" + dependencies: + loose-envify: ^1.0.0 + checksum: 4f2cb6a9575e4faf71ddad9ad1ae7a00d0a75d24521c193fa464f30e6b04027bd97aa5d9546b0e13d3a150ab402eda216d59c1d0f2d6ca60124d96cd40dfa35c + languageName: node + linkType: hard + "watchpack@npm:^2.0.0-beta.10, watchpack@npm:^2.4.0": version: 2.4.0 resolution: "watchpack@npm:2.4.0" @@ -38181,10 +38589,10 @@ __metadata: languageName: node linkType: hard -"web-streams-polyfill@npm:^3.2.0": - version: 3.2.0 - resolution: "web-streams-polyfill@npm:3.2.0" - checksum: e23ad0649392fa0159dbfc6bb27474c308c3f332d9078cfef3c06c154165bef18732c5814126147c6c712f604216ddc950c171c854e3821f020e0d2d721a5958 +"web-streams-polyfill@npm:^3.2.0, web-streams-polyfill@npm:^3.2.1": + version: 3.2.1 + resolution: "web-streams-polyfill@npm:3.2.1" + checksum: b119c78574b6d65935e35098c2afdcd752b84268e18746606af149e3c424e15621b6f1ff0b42b2676dc012fc4f0d313f964b41a4b5031e525faa03997457da02 languageName: node linkType: hard @@ -38295,13 +38703,13 @@ __metadata: languageName: node linkType: hard -"webpack-sources@npm:^2.2.0": - version: 2.3.1 - resolution: "webpack-sources@npm:2.3.1" +"webpack-sources@npm:^1.4.3": + version: 1.4.3 + resolution: "webpack-sources@npm:1.4.3" dependencies: - source-list-map: ^2.0.1 - source-map: ^0.6.1 - checksum: 6fd67f2274a84c5f51ad89767112ec8b47727134bf0f2ba0cff458c970f18966939a24128bdbddba621cd66eeb2bef0552642a9333cd8e54514f7b2a71776346 + source-list-map: ^2.0.0 + source-map: ~0.6.1 + checksum: 37463dad8d08114930f4bc4882a9602941f07c9f0efa9b6bc78738cd936275b990a596d801ef450d022bb005b109b9f451dd087db2f3c9baf53e8e22cf388f79 languageName: node linkType: hard @@ -38385,7 +38793,7 @@ __metadata: languageName: node linkType: hard -"whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1": +"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.4.1": version: 3.6.2 resolution: "whatwg-fetch@npm:3.6.2" checksum: ee976b7249e7791edb0d0a62cd806b29006ad7ec3a3d89145921ad8c00a3a67e4be8f3fb3ec6bc7b58498724fd568d11aeeeea1f7827e7e1e5eae6c8a275afed @@ -38644,7 +39052,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.11.0, ws@npm:^8.4.2, ws@npm:^8.8.0": +"ws@npm:8.11.0": version: 8.11.0 resolution: "ws@npm:8.11.0" peerDependencies: @@ -38674,7 +39082,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^5.2.0 || ^6.0.0 || ^7.0.0, ws@npm:^7.3.1, ws@npm:^7.4.6": +"ws@npm:^5.2.0 || ^6.0.0 || ^7.0.0, ws@npm:^7.4.6": version: 7.5.9 resolution: "ws@npm:7.5.9" peerDependencies: @@ -38689,18 +39097,18 @@ __metadata: languageName: node linkType: hard -"ws@npm:~7.4.2": - version: 7.4.6 - resolution: "ws@npm:7.4.6" +"ws@npm:^8.11.0, ws@npm:^8.4.2, ws@npm:^8.8.0": + version: 8.12.0 + resolution: "ws@npm:8.12.0" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: 3a990b32ed08c72070d5e8913e14dfcd831919205be52a3ff0b4cdd998c8d554f167c9df3841605cde8b11d607768cacab3e823c58c96a5c08c987e093eb767a + checksum: 818ff3f8749c172a95a114cceb8b89cedd27e43a82d65c7ad0f7882b1e96a2ee6709e3746a903c3fa88beec0c8bae9a9fcd75f20858b32a166dfb7519316a5d7 languageName: node linkType: hard @@ -38711,18 +39119,6 @@ __metadata: languageName: node linkType: hard -"xhr@npm:^2.0.1": - version: 2.6.0 - resolution: "xhr@npm:2.6.0" - dependencies: - global: ~4.4.0 - is-function: ^1.0.1 - parse-headers: ^2.0.0 - xtend: ^4.0.0 - checksum: a1db277e37737caf3ed363d2a33ce4b4ea5b5fc190b663a6f70bc252799185b840ccaa166eaeeea4841c9c60b87741f0a24e29cbcf6708dd425986d4df186d2f - languageName: node - linkType: hard - "xml-but-prettier@npm:^1.0.1": version: 1.0.1 resolution: "xml-but-prettier@npm:1.0.1" @@ -38767,13 +39163,6 @@ __metadata: languageName: node linkType: hard -"xml-parse-from-string@npm:^1.0.0": - version: 1.0.1 - resolution: "xml-parse-from-string@npm:1.0.1" - checksum: 5155cb98e428409829f4060ce542c55438b2f7646d11fd306d850eaf12d35c06ffd9e86d76aa5230121a533b958fd1a319d6f90a5c113391853d0ff01f4da7bb - languageName: node - linkType: hard - "xml2js@npm:0.4.19": version: 0.4.19 resolution: "xml2js@npm:0.4.19" @@ -38784,7 +39173,7 @@ __metadata: languageName: node linkType: hard -"xml2js@npm:^0.4.11, xml2js@npm:^0.4.19, xml2js@npm:^0.4.23, xml2js@npm:^0.4.5": +"xml2js@npm:^0.4.11, xml2js@npm:^0.4.19, xml2js@npm:^0.4.23": version: 0.4.23 resolution: "xml2js@npm:0.4.23" dependencies: @@ -38836,13 +39225,6 @@ __metadata: languageName: node linkType: hard -"xmlhttprequest-ssl@npm:~1.6.2": - version: 1.6.3 - resolution: "xmlhttprequest-ssl@npm:1.6.3" - checksum: ac8e5de1cdd170bddb928de75393e8977e7eb80c0d8c24fe4be07f6aa1d5c8e2e42296d29abca6591ec2046cc708c220791ecfa56db43c958b8e4de8e7d39984 - languageName: node - linkType: hard - "xpath@npm:0.0.32": version: 0.0.32 resolution: "xpath@npm:0.0.32" @@ -39047,16 +39429,9 @@ __metadata: languageName: node linkType: hard -"yeast@npm:0.1.2": - version: 0.1.2 - resolution: "yeast@npm:0.1.2" - checksum: 81a250b69f601fed541e9518eb2972e75631dd81231689503d7f288612d4eec793b29c208d6807fd6bfc4c2a43614d0c6db233739a4ae6223e244aaed6a885c0 - languageName: node - linkType: hard - "yeoman-environment@npm:^3.9.1": - version: 3.13.0 - resolution: "yeoman-environment@npm:3.13.0" + version: 3.15.1 + resolution: "yeoman-environment@npm:3.15.1" dependencies: "@npmcli/arborist": ^4.0.4 are-we-there-yet: ^2.0.0 @@ -39099,7 +39474,7 @@ __metadata: mem-fs-editor: ^8.1.2 || ^9.0.0 bin: yoe: cli/index.js - checksum: 2d622d18d2e3fff179477b6dabbdee8b380d58b5d639d300c3ace75e0533223cecb9b325baaf0f46f71879eaa5778d64b11fa660403a26ec3086b2a428484ce6 + checksum: 7ea7bb0a210f2e0702f54420916075cda329ee2b71243fb911af9bcfc1e59a934daef1027dd9e95a410f5dae1eeb1fcdb85f4c697a0429477a3a50d2e48d0d58 languageName: node linkType: hard